diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..e4ffa1f6a Binary files /dev/null and b/.DS_Store differ diff --git a/answers.md b/answers.md index 092b6d280..a48a8feb8 100644 --- a/answers.md +++ b/answers.md @@ -1 +1,326 @@ -Your answers to the questions go here. +My answers to the questions go here. +## Table of Contents +* [Setting Up the Environment](#setting-up-the-environment) +* [Collecting Metrics](#collecting-metrics) +* [Visualizing Data](#visualizing-data) +* [Monitoring Data](#monitoring-data) +* [Collecting APM Data](#collecting-apm-data) +* [Final Question](#final-question) + + + +## Prerequisites - Setting Up the Environment +1. First, I spinned up a fresh linux Ubuntu VM via Vagrant on Virtual Box on my Mac. I followed these steps: https://medium.com/devops-dudes/how-to-setup-vagrant-and-virtual-box-for-ubuntu-20-04-7374bf9cc3fa +2. Next, I signed up for Datadog (used “Datadog Recruiting Candidate” in the “Company” field). +3. And installed the Datadog Agent on the Vagrant Box by running this command for ubuntu: +```DD_AGENT_MAJOR_VERSION=7 DD_API_KEY=########## DD_SITE="datadoghq.com" bash -c "$(curl -L https://s3.amazonaws.com/dd-agent/scripts/install_script.sh)"``` +I removed the key for security purposes. +4. Finally, got the Agent reporting metrics from my local machine: + + + + +## Collecting Metrics +1. Added tags in the Agent config file and here is the screenshot of the host and its tags on the Host Map page in Datadog. + + + + +2. Installed PostgreSQL on the Vagrant machine and then installed the respective Datadog integration for that database. + - Added the public GPG key: + ```wget -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -``` + - Created a file with the repository address: + ```echo deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main | sudo tee /etc/apt/sources.list.d/postgresql.list``` + - To install PostgreSQL I ran: + ```sudo apt-get update``` + ```sudo apt-get install postgresql-9.3 postgresql-contrib-9.3``` + - After installing PostgreSQL, I ran the following command to create a user for Datadog in the database. + ```create user datadog with password ‘######’;``` +3. Created a script file and a config file in order to create a custom metric and send it to Datadog servers through the Agent. + + + +After ```mymetric.py``` was created ```~/.datadog-agent/checks.d/mymetric.py``` added the following to the file: + + + +Then I created ```mymetric.yaml ~/.datadog-agent/conf.d/mymetric.yaml``` and added the code below. + + + +After I restarted the Agent. + + +On the Datadog dashboard, I went to ```Metrics > Explorer```, and searched for my custom metric. +The Agent was running the collector in intervals of 15–20 seconds. + +4. To change my check’s collection interval I edited the config file ```~/.datadog-agent/conf.d/mymetric.yaml``` and changed the ```min_collection_interval``` globally to the interval of 45 seconds. + + + +And restarted the Agent again. + + +5. **Bonus Question:** Can you change the collection interval without modifying the Python check file you created? + **Answer:** Yes, the interval can be set by changing the instance description in the ```yaml``` file, like this: +``` + init_config: + min_collection_interval: 45 + instances: + [{}] +``` + + + +## Visualizing Data +1. This board was created using the PostMan API editor. + +To create my dashboard I used the content of this curl command: + ``` + curl --location --request POST 'https://api.datadoghq.com/api/v1/dashboard' \ + --header 'Content-Type: application/json' \ + --header 'Cookie: DD-PSHARD=217' \ + --header 'DD-API-KEY: xxxxxxxxxxxxxxxxxxxxxxxxxx' \ + --header 'DD-APPLICATION-KEY: xxxxxxxxxxxxxxxxxxxxxxxxxx' \ + --data-raw '{ + "title": "Dev.Zarudnaya Hourly Anomalies", + "description": "A custom agent check configured to submit a metric named `my_metric` with a random value between 0 and 1000.", + "layout_type": "ordered", + "is_read_only": false, + "widgets": [ + { + "definition": { + "type": "timeseries", + "title": "my metric timeseries", + "requests": [ + { + "q": "my_metric{host:datadog.dev.zarudnaya.info}" + } + ] + }, + "layout": { + "x": 0, + "y": 0, + "width": 4, + "height": 2 + } + }, + { + "definition": { + "type": "timeseries", + "title": "Postgres Sql Return Rows Anamolies", + "requests": [ + { + "q": "anomalies(sum:postgresql.rows_returned{host:datadog.dev.zarudnaya.info}, '\''basic'\'', 5)" + } + ] + }, + "layout": { + "x": 4, + "y": 0, + "width": 4, + "height": 2 + } + }, + { + "definition": { + "type": "timeseries", + "title": "Sum of my metric points per hour", + "requests": [ + { + "q": "sum:my_metric{*}.rollup(sum, 3600)" + } + ] + }, + "layout": { + "x": 8, + "y": 0, + "width": 4, + "height": 2 + } + } + ] + }' + ``` + +2. Once this was created, I accessed the Dashboard from the Dashboard List in the UI: + ##### My Hourly Timeboard + + +3. The sum of my metric is grouped into hours (per instructions) so it did not show properly in a 5 minute time span. I expanded the time period on the widget and included a snapshot here. + + + [Hourly Anomalies Dashboard Link](https://p.datadoghq.com/sb/ab7cb560-3783-11ec-9ab2-da7ad0900002-6fcd0e111c4fd4be11af2203756fb0b8) + +4. Took a snapshot of this graph and used the @ notation to send it to myself. The email notice included a thumbnail, and also buttons that take you directly to the item in the Datadog panel. + + +5. **Bonus Question:** What is the Anomaly graph displaying? + **Answer:** The Anomaly graph presents suspicious behaviour. I stressed PostgreSQL and made the tables list, thus, causing it to perform in a certain way. The graph provides a visualization of this performance as it relates to PostgreSQL. + + + +## Monitoring Data +#### Create a New Metric: +1. From Datadog dashboard ```Monitors > New Monitor > Metric Monitor > Metric``` I defined the specs for ```my_metric``` based on the requirements. + - Changed the metric to the custom metric ``my_metric``. + - Changed the warning threshold to 500. + - Changed the alerting threshold to 800. + - Ensured notifications for No Data past 10 minutes. + +#### Monitor Metrics: + + + +#### Configure the Monitor's Message: +2. I configured the monitor message with below settings: + + +#### Monitor Message Email Notifications +3. Created different messages based on whether the monitor is in an Alert, Warning, or No Data state. + +#### Alert: + + +#### Warning: + + +#### Missing Data: + + +**Bonus Question:** Since this monitor is going to alert pretty often, you don’t want to be alerted when you are out of the office. Set up two scheduled downtimes for this monitor: +#### Downtime Settings: +1. From Datadog dashboard ```Monitors > Manage Downtime``` clicked 'Schedule Downtime': + + + +2. Created a recurring downtime for days of the week and weekends. I used EST since this is my time zone and Datadog HQ time zone. +#### Downtime Weekday Settings: + + +#### Downtime Weekend Settings: + + + + +## Collecting APM Data +1. For this exercise I created a basic **To Do** Flask app on my Vagrant VM using this command: +```pip install Flask```, for the database I used Flask-SQLAlchemy using this command: +```pip install Flask-SQLAlchemy```. + +2. Created a ```venv``` environment for the Flask app on the Vagrant machine. I installed DDtrace module using this command: +```pip install ddtrace``` and enabled APM in the Datadog ```yaml``` file: + + + +3. Injected ```ddtrace``` in the ```app.py```: +``` +from flask import Flask, render_template, request, redirect, url_for +from flask_sqlalchemy import SQLAlchemy +from ddtrace import tracer + +app = Flask(__name__) + +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +db = SQLAlchemy(app) + +class Todo(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(100)) + complete = db.Column(db.Boolean) + +@app.route("/") +def home(): + todo_list = Todo.query.all() + return render_template("base.html", todo_list=todo_list) + +@app.route("/add", methods=["POST"]) +@tracer.wrap("flask.request", service='flask', resource="POST /add", span_type='web') +def add(): + title = request.form.get("title") + new_todo = Todo(title=title, complete=False) + db.session.add(new_todo) + db.session.commit() + return redirect(url_for("home")) + +@app.route("/update/") +def update(todo_id): + todo = Todo.query.filter_by(id=todo_id).first() + todo.complete = not todo.complete + db.session.commit() + return redirect(url_for("home")) + +@app.route("/delete/") +def delete(todo_id): + todo = Todo.query.filter_by(id=todo_id).first() + db.session.delete(todo) + db.session.commit() + return redirect(url_for("home")) + +if __name__ == "__main__": + db.create_all() + app.run(debug=True) + app.run(host='0.0.0.0', port='5000') +``` +4. I ran this command to start tracking APM logs: +```DD_SERVICE="app" DD_ENV="dev" DD_LOGS_INJECTION=true ddtrace-run python3 app.py``` + + + +5. In order for my Flask app on port 5000 to run I modified the agent port to 5050: + + + +My To Do App on the browser: + + +APM dashboard on Datadog website: + + + +6. Created some entries in my To Do APP and it is showing on the APM Dashboard: + + + +#### Dashboard with both APM and Infrastructure Metrics: + +[Dashboard APM link](https://p.datadoghq.com/sb/ab7cb560-3783-11ec-9ab2-da7ad0900002-b486ce8c5579c41542cd9f0d78eed153) (Flask app was run on November, 21st. Please change the time range to 1 month to see the results) + +7. **Bonus Question:** What is the difference between a Service and a Resource? + **Answer:** Services are the building blocks of modern microservice architectures. A service groups together endpoints. + A resource is an action given to a service, such as a query to a database or an endpoint. + [Reference](https://docs.datadoghq.com/tracing/visualization/#pagetitle) + + + +## Final Question +Datadog has been used in a lot of creative ways in the past. We’ve written some blog posts about using Datadog to monitor the NYC Subway System, Pokemon Go, and even office restroom availability! + +Is there anything creative you would use Datadog for? + +**Answer:** Datadog can be used in many creative ways ([Datadog in the wild: 5 fun projects](https://www.datadoghq.com/blog/datadog-in-the-wild-5-fun-projects/)). In this pandemic times lots of people are dependent on medical equipment. For example, medical systems could use Datadog monitoring to ensure that they are functioning correctly in case of an emergency or an unforeseen event and alert for any anomalies. +Another example how I could use Datadog is to compare prices on different e-commerce websites and select the cheapest price. It could also help to monitor information regarding sales and items that are back in stock. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flask-app/__pycache__/app.cpython-36.pyc b/flask-app/__pycache__/app.cpython-36.pyc new file mode 100644 index 000000000..17d6d7b62 Binary files /dev/null and b/flask-app/__pycache__/app.cpython-36.pyc differ diff --git a/flask-app/app.py b/flask-app/app.py new file mode 100644 index 000000000..1a0a65f99 --- /dev/null +++ b/flask-app/app.py @@ -0,0 +1,47 @@ +from flask import Flask, render_template, request, redirect, url_for +from flask_sqlalchemy import SQLAlchemy +from ddtrace import tracer + +app = Flask(__name__) + +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +db = SQLAlchemy(app) + +class Todo(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(100)) + complete = db.Column(db.Boolean) + +@app.route("/") +def home(): + todo_list = Todo.query.all() + return render_template("base.html", todo_list=todo_list) + +@app.route("/add", methods=["POST"]) +@tracer.wrap("flask.request", service='flask', resource="POST /add", span_type='web') +def add(): + title = request.form.get("title") + new_todo = Todo(title=title, complete=False) + db.session.add(new_todo) + db.session.commit() + return redirect(url_for("home")) + +@app.route("/update/") +def update(todo_id): + todo = Todo.query.filter_by(id=todo_id).first() + todo.complete = not todo.complete + db.session.commit() + return redirect(url_for("home")) + +@app.route("/delete/") +def delete(todo_id): + todo = Todo.query.filter_by(id=todo_id).first() + db.session.delete(todo) + db.session.commit() + return redirect(url_for("home")) + +if __name__ == "__main__": + db.create_all() + app.run(debug=True) + app.run(host='0.0.0.0', port='5000') diff --git a/flask-app/db.sqlite b/flask-app/db.sqlite new file mode 100644 index 000000000..60d0725f7 Binary files /dev/null and b/flask-app/db.sqlite differ diff --git a/flask-app/templates/base.html b/flask-app/templates/base.html new file mode 100644 index 000000000..b40815c08 --- /dev/null +++ b/flask-app/templates/base.html @@ -0,0 +1,44 @@ + + + + + + + Todo App + + + + + + +
+

To Do App

+ +
+
+ +
+
+ +
+ +
+ + {% for todo in todo_list %} +
+

{{todo.id }} | {{ todo.title }}

+ + {% if todo.complete == False %} + Not Complete + {% else %} + Completed + {% endif %} + +
Update + Delete +
+ {% endfor %} +
+ + + \ No newline at end of file diff --git a/flask-app/venv/bin/Activate.ps1 b/flask-app/venv/bin/Activate.ps1 new file mode 100644 index 000000000..5b9adb2b7 --- /dev/null +++ b/flask-app/venv/bin/Activate.ps1 @@ -0,0 +1,231 @@ +<# +.Synopsis +Activate a Python virtual environment for the current Powershell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0,1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + $VenvDir = $VenvDir.Insert($VenvDir.Length, "/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virutal environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/flask-app/venv/bin/activate b/flask-app/venv/bin/activate new file mode 100644 index 000000000..83c50e639 --- /dev/null +++ b/flask-app/venv/bin/activate @@ -0,0 +1,76 @@ +# This file must be used with "source bin/activate" *from bash* +# you cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # This should detect bash and zsh, which have a hash command that must + # be called to get it to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r + fi + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +VIRTUAL_ENV="/Users/Kate/Datadog/myapp/venv" +export VIRTUAL_ENV + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/bin:$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + if [ "x(venv) " != x ] ; then + PS1="(venv) ${PS1:-}" + else + if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then + # special case for Aspen magic directories + # see http://www.zetadev.com/software/aspen/ + PS1="[`basename \`dirname \"$VIRTUAL_ENV\"\``] $PS1" + else + PS1="(`basename \"$VIRTUAL_ENV\"`)$PS1" + fi + fi + export PS1 +fi + +# This should detect bash and zsh, which have a hash command that must +# be called to get it to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r +fi diff --git a/flask-app/venv/bin/activate.csh b/flask-app/venv/bin/activate.csh new file mode 100644 index 000000000..b8f8cd76f --- /dev/null +++ b/flask-app/venv/bin/activate.csh @@ -0,0 +1,37 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV "/Users/Kate/Datadog/myapp/venv" + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/bin:$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + if ("venv" != "") then + set env_name = "venv" + else + if (`basename "VIRTUAL_ENV"` == "__") then + # special case for Aspen magic directories + # see http://www.zetadev.com/software/aspen/ + set env_name = `basename \`dirname "$VIRTUAL_ENV"\`` + else + set env_name = `basename "$VIRTUAL_ENV"` + endif + endif + set prompt = "[$env_name] $prompt" + unset env_name +endif + +alias pydoc python -m pydoc + +rehash diff --git a/flask-app/venv/bin/activate.fish b/flask-app/venv/bin/activate.fish new file mode 100644 index 000000000..84e7d785e --- /dev/null +++ b/flask-app/venv/bin/activate.fish @@ -0,0 +1,75 @@ +# This file must be used with ". bin/activate.fish" *from fish* (http://fishshell.org) +# you cannot run it directly + +function deactivate -d "Exit virtualenv and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + functions -e fish_prompt + set -e _OLD_FISH_PROMPT_OVERRIDE + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + + set -e VIRTUAL_ENV + if test "$argv[1]" != "nondestructive" + # Self destruct! + functions -e deactivate + end +end + +# unset irrelevant variables +deactivate nondestructive + +set -gx VIRTUAL_ENV "/Users/Kate/Datadog/myapp/venv" + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/bin" $PATH + +# unset PYTHONHOME if set +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # save the current fish_prompt function as the function _old_fish_prompt + functions -c fish_prompt _old_fish_prompt + + # with the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command + set -l old_status $status + + # Prompt override? + if test -n "(venv) " + printf "%s%s" "(venv) " (set_color normal) + else + # ...Otherwise, prepend env + set -l _checkbase (basename "$VIRTUAL_ENV") + if test $_checkbase = "__" + # special case for Aspen magic directories + # see http://www.zetadev.com/software/aspen/ + printf "%s[%s]%s " (set_color -b blue white) (basename (dirname "$VIRTUAL_ENV")) (set_color normal) + else + printf "%s(%s)%s" (set_color -b blue white) (basename "$VIRTUAL_ENV") (set_color normal) + end + end + + # Restore the return status of the previous command. + echo "exit $old_status" | . + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" +end diff --git a/flask-app/venv/bin/ddtrace-run b/flask-app/venv/bin/ddtrace-run new file mode 100755 index 000000000..c36cae3a3 --- /dev/null +++ b/flask-app/venv/bin/ddtrace-run @@ -0,0 +1,10 @@ +#!/Users/Kate/Datadog/myapp/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys + +from ddtrace.commands.ddtrace_run import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/flask-app/venv/bin/easy_install b/flask-app/venv/bin/easy_install new file mode 100755 index 000000000..b4a4d31fc --- /dev/null +++ b/flask-app/venv/bin/easy_install @@ -0,0 +1,10 @@ +#!/Users/Kate/Datadog/myapp/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys + +from setuptools.command.easy_install import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/flask-app/venv/bin/easy_install-3.8 b/flask-app/venv/bin/easy_install-3.8 new file mode 100755 index 000000000..b4a4d31fc --- /dev/null +++ b/flask-app/venv/bin/easy_install-3.8 @@ -0,0 +1,10 @@ +#!/Users/Kate/Datadog/myapp/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys + +from setuptools.command.easy_install import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/flask-app/venv/bin/flask b/flask-app/venv/bin/flask new file mode 100755 index 000000000..8e24564b3 --- /dev/null +++ b/flask-app/venv/bin/flask @@ -0,0 +1,10 @@ +#!/Users/Kate/Datadog/myapp/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys + +from flask.cli import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/flask-app/venv/bin/pip b/flask-app/venv/bin/pip new file mode 100755 index 000000000..f30b86249 --- /dev/null +++ b/flask-app/venv/bin/pip @@ -0,0 +1,10 @@ +#!/Users/Kate/Datadog/myapp/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys + +from pip._internal import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/flask-app/venv/bin/pip3 b/flask-app/venv/bin/pip3 new file mode 100755 index 000000000..f30b86249 --- /dev/null +++ b/flask-app/venv/bin/pip3 @@ -0,0 +1,10 @@ +#!/Users/Kate/Datadog/myapp/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys + +from pip._internal import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/flask-app/venv/bin/pip3.8 b/flask-app/venv/bin/pip3.8 new file mode 100755 index 000000000..f30b86249 --- /dev/null +++ b/flask-app/venv/bin/pip3.8 @@ -0,0 +1,10 @@ +#!/Users/Kate/Datadog/myapp/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys + +from pip._internal import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/flask-app/venv/bin/python b/flask-app/venv/bin/python new file mode 120000 index 000000000..b8a0adbbb --- /dev/null +++ b/flask-app/venv/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/flask-app/venv/bin/python3 b/flask-app/venv/bin/python3 new file mode 120000 index 000000000..3b8210b56 --- /dev/null +++ b/flask-app/venv/bin/python3 @@ -0,0 +1 @@ +/Library/Frameworks/Python.framework/Versions/3.8/bin/python3 \ No newline at end of file diff --git a/flask-app/venv/include/site/python3.8/greenlet/greenlet.h b/flask-app/venv/include/site/python3.8/greenlet/greenlet.h new file mode 100644 index 000000000..830bef8dd --- /dev/null +++ b/flask-app/venv/include/site/python3.8/greenlet/greenlet.h @@ -0,0 +1,146 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ + +/* Greenlet object interface */ + +#ifndef Py_GREENLETOBJECT_H +#define Py_GREENLETOBJECT_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* This is deprecated and undocumented. It does not change. */ +#define GREENLET_VERSION "1.0.0" + +typedef struct _greenlet { + PyObject_HEAD + char* stack_start; + char* stack_stop; + char* stack_copy; + intptr_t stack_saved; + struct _greenlet* stack_prev; + struct _greenlet* parent; + PyObject* run_info; + struct _frame* top_frame; + int recursion_depth; + PyObject* weakreflist; +#if PY_VERSION_HEX >= 0x030700A3 + _PyErr_StackItem* exc_info; + _PyErr_StackItem exc_state; +#else + PyObject* exc_type; + PyObject* exc_value; + PyObject* exc_traceback; +#endif + PyObject* dict; +#if PY_VERSION_HEX >= 0x030700A3 + PyObject* context; +#endif +#if PY_VERSION_HEX >= 0x30A00B1 + CFrame* cframe; +#endif +} PyGreenlet; + +#define PyGreenlet_Check(op) PyObject_TypeCheck(op, &PyGreenlet_Type) +#define PyGreenlet_MAIN(op) (((PyGreenlet*)(op))->stack_stop == (char*)-1) +#define PyGreenlet_STARTED(op) (((PyGreenlet*)(op))->stack_stop != NULL) +#define PyGreenlet_ACTIVE(op) (((PyGreenlet*)(op))->stack_start != NULL) +#define PyGreenlet_GET_PARENT(op) (((PyGreenlet*)(op))->parent) + +/* C API functions */ + +/* Total number of symbols that are exported */ +#define PyGreenlet_API_pointers 8 + +#define PyGreenlet_Type_NUM 0 +#define PyExc_GreenletError_NUM 1 +#define PyExc_GreenletExit_NUM 2 + +#define PyGreenlet_New_NUM 3 +#define PyGreenlet_GetCurrent_NUM 4 +#define PyGreenlet_Throw_NUM 5 +#define PyGreenlet_Switch_NUM 6 +#define PyGreenlet_SetParent_NUM 7 + +#ifndef GREENLET_MODULE +/* This section is used by modules that uses the greenlet C API */ +static void** _PyGreenlet_API = NULL; + +# define PyGreenlet_Type \ + (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM]) + +# define PyExc_GreenletError \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM]) + +# define PyExc_GreenletExit \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM]) + +/* + * PyGreenlet_New(PyObject *args) + * + * greenlet.greenlet(run, parent=None) + */ +# define PyGreenlet_New \ + (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \ + _PyGreenlet_API[PyGreenlet_New_NUM]) + +/* + * PyGreenlet_GetCurrent(void) + * + * greenlet.getcurrent() + */ +# define PyGreenlet_GetCurrent \ + (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) + +/* + * PyGreenlet_Throw( + * PyGreenlet *greenlet, + * PyObject *typ, + * PyObject *val, + * PyObject *tb) + * + * g.throw(...) + */ +# define PyGreenlet_Throw \ + (*(PyObject * (*)(PyGreenlet * self, \ + PyObject * typ, \ + PyObject * val, \ + PyObject * tb)) \ + _PyGreenlet_API[PyGreenlet_Throw_NUM]) + +/* + * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) + * + * g.switch(*args, **kwargs) + */ +# define PyGreenlet_Switch \ + (*(PyObject * \ + (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \ + _PyGreenlet_API[PyGreenlet_Switch_NUM]) + +/* + * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) + * + * g.parent = new_parent + */ +# define PyGreenlet_SetParent \ + (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \ + _PyGreenlet_API[PyGreenlet_SetParent_NUM]) + +/* Macro that imports greenlet and initializes C API */ +/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we + keep the older definition to be sure older code that might have a copy of + the header still works. */ +# define PyGreenlet_Import() \ + { \ + _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ + } + +#endif /* GREENLET_MODULE */ + +#ifdef __cplusplus +} +#endif +#endif /* !Py_GREENLETOBJECT_H */ diff --git a/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/INSTALLER b/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/LICENSE.rst b/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/LICENSE.rst new file mode 100644 index 000000000..9d227a0cc --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/LICENSE.rst @@ -0,0 +1,28 @@ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/METADATA b/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/METADATA new file mode 100644 index 000000000..aaf27caed --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/METADATA @@ -0,0 +1,125 @@ +Metadata-Version: 2.1 +Name: Flask +Version: 2.0.2 +Summary: A simple framework for building complex web applications. +Home-page: https://palletsprojects.com/p/flask +Author: Armin Ronacher +Author-email: armin.ronacher@active-4.com +Maintainer: Pallets +Maintainer-email: contact@palletsprojects.com +License: BSD-3-Clause +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Documentation, https://flask.palletsprojects.com/ +Project-URL: Changes, https://flask.palletsprojects.com/changes/ +Project-URL: Source Code, https://github.com/pallets/flask/ +Project-URL: Issue Tracker, https://github.com/pallets/flask/issues/ +Project-URL: Twitter, https://twitter.com/PalletsTeam +Project-URL: Chat, https://discord.gg/pallets +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Framework :: Flask +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application +Classifier: Topic :: Software Development :: Libraries :: Application Frameworks +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst +License-File: LICENSE.rst +Requires-Dist: Werkzeug (>=2.0) +Requires-Dist: Jinja2 (>=3.0) +Requires-Dist: itsdangerous (>=2.0) +Requires-Dist: click (>=7.1.2) +Provides-Extra: async +Requires-Dist: asgiref (>=3.2) ; extra == 'async' +Provides-Extra: dotenv +Requires-Dist: python-dotenv ; extra == 'dotenv' + +Flask +===== + +Flask is a lightweight `WSGI`_ web application framework. It is designed +to make getting started quick and easy, with the ability to scale up to +complex applications. It began as a simple wrapper around `Werkzeug`_ +and `Jinja`_ and has become one of the most popular Python web +application frameworks. + +Flask offers suggestions, but doesn't enforce any dependencies or +project layout. It is up to the developer to choose the tools and +libraries they want to use. There are many extensions provided by the +community that make adding new functionality easy. + +.. _WSGI: https://wsgi.readthedocs.io/ +.. _Werkzeug: https://werkzeug.palletsprojects.com/ +.. _Jinja: https://jinja.palletsprojects.com/ + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + $ pip install -U Flask + +.. _pip: https://pip.pypa.io/en/stable/getting-started/ + + +A Simple Example +---------------- + +.. code-block:: python + + # save this as app.py + from flask import Flask + + app = Flask(__name__) + + @app.route("/") + def hello(): + return "Hello, World!" + +.. code-block:: text + + $ flask run + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) + + +Contributing +------------ + +For guidance on setting up a development environment and how to make a +contribution to Flask, see the `contributing guidelines`_. + +.. _contributing guidelines: https://github.com/pallets/flask/blob/main/CONTRIBUTING.rst + + +Donate +------ + +The Pallets organization develops and supports Flask and the libraries +it uses. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, `please +donate today`_. + +.. _please donate today: https://palletsprojects.com/donate + + +Links +----- + +- Documentation: https://flask.palletsprojects.com/ +- Changes: https://flask.palletsprojects.com/changes/ +- PyPI Releases: https://pypi.org/project/Flask/ +- Source Code: https://github.com/pallets/flask/ +- Issue Tracker: https://github.com/pallets/flask/issues/ +- Website: https://palletsprojects.com/p/flask/ +- Twitter: https://twitter.com/PalletsTeam +- Chat: https://discord.gg/pallets + + diff --git a/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/RECORD b/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/RECORD new file mode 100644 index 000000000..c87e92b92 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/RECORD @@ -0,0 +1,51 @@ +../../../bin/flask,sha256=xBFcdARdhz83LcheIPBWDYpWv0OTH3qBfBco8aJQ9aI,237 +Flask-2.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +Flask-2.0.2.dist-info/LICENSE.rst,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475 +Flask-2.0.2.dist-info/METADATA,sha256=aKsvjFA_ZjZN1jLh1Ac3aQk-ZUZDPrrwo_TGYW1kdAQ,3839 +Flask-2.0.2.dist-info/RECORD,, +Flask-2.0.2.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +Flask-2.0.2.dist-info/entry_points.txt,sha256=gBLA1aKg0OYR8AhbAfg8lnburHtKcgJLDU52BBctN0k,42 +Flask-2.0.2.dist-info/top_level.txt,sha256=dvi65F6AeGWVU0TBpYiC04yM60-FX1gJFkK31IKQr5c,6 +flask/__init__.py,sha256=9ZCelLoNCpr6eSuLmYlzvbp12B3lrLgoN5U2UWk1vdo,2251 +flask/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30 +flask/__pycache__/__init__.cpython-38.pyc,, +flask/__pycache__/__main__.cpython-38.pyc,, +flask/__pycache__/app.cpython-38.pyc,, +flask/__pycache__/blueprints.cpython-38.pyc,, +flask/__pycache__/cli.cpython-38.pyc,, +flask/__pycache__/config.cpython-38.pyc,, +flask/__pycache__/ctx.cpython-38.pyc,, +flask/__pycache__/debughelpers.cpython-38.pyc,, +flask/__pycache__/globals.cpython-38.pyc,, +flask/__pycache__/helpers.cpython-38.pyc,, +flask/__pycache__/logging.cpython-38.pyc,, +flask/__pycache__/scaffold.cpython-38.pyc,, +flask/__pycache__/sessions.cpython-38.pyc,, +flask/__pycache__/signals.cpython-38.pyc,, +flask/__pycache__/templating.cpython-38.pyc,, +flask/__pycache__/testing.cpython-38.pyc,, +flask/__pycache__/typing.cpython-38.pyc,, +flask/__pycache__/views.cpython-38.pyc,, +flask/__pycache__/wrappers.cpython-38.pyc,, +flask/app.py,sha256=ectBbi9hGmVHAse5TNcFQZIDRkDAxYUAnLgfuKD0Xws,81975 +flask/blueprints.py,sha256=AkAVXZ_MMkjwjklzCAMdBNowTiM0wVQPynnUnXjTL2M,23781 +flask/cli.py,sha256=wn2Un9RO32ZfRmCMem5KJ5h62-5lnmy1H9uxgyV-eBs,32238 +flask/config.py,sha256=70Uyjh1Jzb9MfTCT7NDhuZWAzyIEu-TIyk6-22MP3zQ,11285 +flask/ctx.py,sha256=EM3W0v1ctuFQAGk_HWtQdoJEg_r2f5Le4xcmElxFwwk,17428 +flask/debughelpers.py,sha256=W82-xrRmodjopBngI9roYH-q08EbQwN2HEGfDAi6SA0,6184 +flask/globals.py,sha256=cWd-R2hUH3VqPhnmQNww892tQS6Yjqg_wg8UvW1M7NM,1723 +flask/helpers.py,sha256=00WqA3wYeyjMrnAOPZTUyrnUf7H8ik3CVT0kqGl_qjk,30589 +flask/json/__init__.py,sha256=unAKdZBlxMI5OMiTU0-Z2Hl4CF1CMJmqTUzpStiExNw,11822 +flask/json/__pycache__/__init__.cpython-38.pyc,, +flask/json/__pycache__/tag.cpython-38.pyc,, +flask/json/tag.py,sha256=fys3HBLssWHuMAIJuTcf2K0bCtosePBKXIWASZEEjnU,8857 +flask/logging.py,sha256=1o_hirVGqdj7SBdETnhX7IAjklG89RXlrwz_2CjzQQE,2273 +flask/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +flask/scaffold.py,sha256=fM9mRy7QBh9fhJ0VTogVx900dDa5oxz8FOw6OK5F-TU,32796 +flask/sessions.py,sha256=Kb7zY4qBIOU2cw1xM5mQ_KmgYUBDFbUYWjlkq0EFYis,15189 +flask/signals.py,sha256=H7QwDciK-dtBxinjKpexpglP0E6k0MJILiFWTItfmqU,2136 +flask/templating.py,sha256=l96VD39JQ0nue4Bcj7wZ4-FWWs-ppLxvgBCpwDQ4KAk,5626 +flask/testing.py,sha256=OsHT-2B70abWH3ulY9IbhLchXIeyj3L-cfcDa88wv5E,10281 +flask/typing.py,sha256=hXEVcXoH-QEabmy1F11pYaQ2SonlkMAwfjBAnqj2x18,1982 +flask/views.py,sha256=nhq31TRB5Z-z2mjFGZACaaB2Et5XPCmWhWxJxOvLWww,5948 +flask/wrappers.py,sha256=VndbHPRBSUUOejmd2Y3ydkoCVUtsS2OJIdJEVIkBVD8,5604 diff --git a/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/WHEEL b/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/WHEEL new file mode 100644 index 000000000..5bad85fdc --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/entry_points.txt b/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/entry_points.txt new file mode 100644 index 000000000..1eb025200 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +flask = flask.cli:main + diff --git a/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/top_level.txt b/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/top_level.txt new file mode 100644 index 000000000..7e1060246 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Flask-2.0.2.dist-info/top_level.txt @@ -0,0 +1 @@ +flask diff --git a/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/INSTALLER b/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/LICENSE.rst b/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/LICENSE.rst new file mode 100644 index 000000000..9d227a0cc --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/LICENSE.rst @@ -0,0 +1,28 @@ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/METADATA b/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/METADATA new file mode 100644 index 000000000..a1971dc2d --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/METADATA @@ -0,0 +1,94 @@ +Metadata-Version: 2.1 +Name: Flask-SQLAlchemy +Version: 2.5.1 +Summary: Adds SQLAlchemy support to your Flask application. +Home-page: https://github.com/pallets/flask-sqlalchemy +Author: Armin Ronacher +Author-email: armin.ronacher@active-4.com +Maintainer: Pallets +Maintainer-email: contact@palletsprojects.com +License: BSD-3-Clause +Project-URL: Documentation, https://flask-sqlalchemy.palletsprojects.com/ +Project-URL: Code, https://github.com/pallets/flask-sqlalchemy +Project-URL: Issue tracker, https://github.com/pallets/flask-sqlalchemy/issues +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >= 2.7, != 3.0.*, != 3.1.*, != 3.2.*, != 3.3.* +Requires-Dist: Flask (>=0.10) +Requires-Dist: SQLAlchemy (>=0.8.0) + +Flask-SQLAlchemy +================ + +Flask-SQLAlchemy is an extension for `Flask`_ that adds support for +`SQLAlchemy`_ to your application. It aims to simplify using SQLAlchemy +with Flask by providing useful defaults and extra helpers that make it +easier to accomplish common tasks. + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + $ pip install -U Flask-SQLAlchemy + + +A Simple Example +---------------- + +.. code-block:: python + + from flask import Flask + from flask_sqlalchemy import SQLAlchemy + + app = Flask(__name__) + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///example.sqlite" + db = SQLAlchemy(app) + + + class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String, unique=True, nullable=False) + email = db.Column(db.String, unique=True, nullable=False) + + + db.session.add(User(name="Flask", email="example@example.com")) + db.session.commit() + + users = User.query.all() + + +Links +----- + +- Documentation: https://flask-sqlalchemy.palletsprojects.com/ +- Releases: https://pypi.org/project/Flask-SQLAlchemy/ +- Code: https://github.com/pallets/flask-sqlalchemy +- Issue tracker: https://github.com/pallets/flask-sqlalchemy/issues +- Test status: https://travis-ci.org/pallets/flask-sqlalchemy +- Test coverage: https://codecov.io/gh/pallets/flask-sqlalchemy + +.. _Flask: https://palletsprojects.com/p/flask/ +.. _SQLAlchemy: https://www.sqlalchemy.org +.. _pip: https://pip.pypa.io/en/stable/quickstart/ + + diff --git a/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/RECORD b/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/RECORD new file mode 100644 index 000000000..69abba111 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/RECORD @@ -0,0 +1,14 @@ +Flask_SQLAlchemy-2.5.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +Flask_SQLAlchemy-2.5.1.dist-info/LICENSE.rst,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475 +Flask_SQLAlchemy-2.5.1.dist-info/METADATA,sha256=vVCeMtTM_xOrUVoVyemeNaTUI5L9iXa16NsiMDDOgFU,3128 +Flask_SQLAlchemy-2.5.1.dist-info/RECORD,, +Flask_SQLAlchemy-2.5.1.dist-info/WHEEL,sha256=Z-nyYpwrcSqxfdux5Mbn_DQ525iP7J2DG3JgGvOYyTQ,110 +Flask_SQLAlchemy-2.5.1.dist-info/top_level.txt,sha256=w2K4fNNoTh4HItoFfz2FRQShSeLcvHYrzU_sZov21QU,17 +flask_sqlalchemy/__init__.py,sha256=IaupgTRkQnY05KPLYvfiNnJdrmwoyfsxaiyGtrEYfO4,40738 +flask_sqlalchemy/__pycache__/__init__.cpython-38.pyc,, +flask_sqlalchemy/__pycache__/_compat.cpython-38.pyc,, +flask_sqlalchemy/__pycache__/model.cpython-38.pyc,, +flask_sqlalchemy/__pycache__/utils.cpython-38.pyc,, +flask_sqlalchemy/_compat.py,sha256=yua0ZSgVWwi56QpEgwaPInzkNQ9PFb7YQdvEk3dImXo,821 +flask_sqlalchemy/model.py,sha256=bd2mIv9LA1A2MZkQObgnMUCSrxNvyqplaSkCxyxKNxY,4988 +flask_sqlalchemy/utils.py,sha256=4eHqAbYElnJ3NbSAHhuINckoAHDABoxjleMJD0iKgyg,1390 diff --git a/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/WHEEL b/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/WHEEL new file mode 100644 index 000000000..01b8fc7d4 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.36.2) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/top_level.txt b/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/top_level.txt new file mode 100644 index 000000000..8a5538e94 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Flask_SQLAlchemy-2.5.1.dist-info/top_level.txt @@ -0,0 +1 @@ +flask_sqlalchemy diff --git a/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/INSTALLER b/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/LICENSE.rst b/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/LICENSE.rst new file mode 100644 index 000000000..c37cae49e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/LICENSE.rst @@ -0,0 +1,28 @@ +Copyright 2007 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/METADATA b/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/METADATA new file mode 100644 index 000000000..3b9355ac0 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/METADATA @@ -0,0 +1,113 @@ +Metadata-Version: 2.1 +Name: Jinja2 +Version: 3.0.3 +Summary: A very fast and expressive template engine. +Home-page: https://palletsprojects.com/p/jinja/ +Author: Armin Ronacher +Author-email: armin.ronacher@active-4.com +Maintainer: Pallets +Maintainer-email: contact@palletsprojects.com +License: BSD-3-Clause +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Documentation, https://jinja.palletsprojects.com/ +Project-URL: Changes, https://jinja.palletsprojects.com/changes/ +Project-URL: Source Code, https://github.com/pallets/jinja/ +Project-URL: Issue Tracker, https://github.com/pallets/jinja/issues/ +Project-URL: Twitter, https://twitter.com/PalletsTeam +Project-URL: Chat, https://discord.gg/pallets +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Text Processing :: Markup :: HTML +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst +License-File: LICENSE.rst +Requires-Dist: MarkupSafe (>=2.0) +Provides-Extra: i18n +Requires-Dist: Babel (>=2.7) ; extra == 'i18n' + +Jinja +===== + +Jinja is a fast, expressive, extensible templating engine. Special +placeholders in the template allow writing code similar to Python +syntax. Then the template is passed data to render the final document. + +It includes: + +- Template inheritance and inclusion. +- Define and import macros within templates. +- HTML templates can use autoescaping to prevent XSS from untrusted + user input. +- A sandboxed environment can safely render untrusted templates. +- AsyncIO support for generating templates and calling async + functions. +- I18N support with Babel. +- Templates are compiled to optimized Python code just-in-time and + cached, or can be compiled ahead-of-time. +- Exceptions point to the correct line in templates to make debugging + easier. +- Extensible filters, tests, functions, and even syntax. + +Jinja's philosophy is that while application logic belongs in Python if +possible, it shouldn't make the template designer's job difficult by +restricting functionality too much. + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + $ pip install -U Jinja2 + +.. _pip: https://pip.pypa.io/en/stable/getting-started/ + + +In A Nutshell +------------- + +.. code-block:: jinja + + {% extends "base.html" %} + {% block title %}Members{% endblock %} + {% block content %} + + {% endblock %} + + +Donate +------ + +The Pallets organization develops and supports Jinja and other popular +packages. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, `please +donate today`_. + +.. _please donate today: https://palletsprojects.com/donate + + +Links +----- + +- Documentation: https://jinja.palletsprojects.com/ +- Changes: https://jinja.palletsprojects.com/changes/ +- PyPI Releases: https://pypi.org/project/Jinja2/ +- Source Code: https://github.com/pallets/jinja/ +- Issue Tracker: https://github.com/pallets/jinja/issues/ +- Website: https://palletsprojects.com/p/jinja/ +- Twitter: https://twitter.com/PalletsTeam +- Chat: https://discord.gg/pallets + + diff --git a/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/RECORD b/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/RECORD new file mode 100644 index 000000000..e60c78b8f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/RECORD @@ -0,0 +1,58 @@ +Jinja2-3.0.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +Jinja2-3.0.3.dist-info/LICENSE.rst,sha256=O0nc7kEF6ze6wQ-vG-JgQI_oXSUrjp3y4JefweCUQ3s,1475 +Jinja2-3.0.3.dist-info/METADATA,sha256=uvKoBSMLvh0qHK-6khEqSe1yOV4jxFzbPSREOp-3BXk,3539 +Jinja2-3.0.3.dist-info/RECORD,, +Jinja2-3.0.3.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +Jinja2-3.0.3.dist-info/entry_points.txt,sha256=Qy_DkVo6Xj_zzOtmErrATe8lHZhOqdjpt3e4JJAGyi8,61 +Jinja2-3.0.3.dist-info/top_level.txt,sha256=PkeVWtLb3-CqjWi1fO29OCbj55EhX_chhKrCdrVe_zs,7 +jinja2/__init__.py,sha256=V3JjnTV-nyIHN6rwj03N1M11fegjGvv-weiHMQwH1pk,2205 +jinja2/__pycache__/__init__.cpython-38.pyc,, +jinja2/__pycache__/_identifier.cpython-38.pyc,, +jinja2/__pycache__/async_utils.cpython-38.pyc,, +jinja2/__pycache__/bccache.cpython-38.pyc,, +jinja2/__pycache__/compiler.cpython-38.pyc,, +jinja2/__pycache__/constants.cpython-38.pyc,, +jinja2/__pycache__/debug.cpython-38.pyc,, +jinja2/__pycache__/defaults.cpython-38.pyc,, +jinja2/__pycache__/environment.cpython-38.pyc,, +jinja2/__pycache__/exceptions.cpython-38.pyc,, +jinja2/__pycache__/ext.cpython-38.pyc,, +jinja2/__pycache__/filters.cpython-38.pyc,, +jinja2/__pycache__/idtracking.cpython-38.pyc,, +jinja2/__pycache__/lexer.cpython-38.pyc,, +jinja2/__pycache__/loaders.cpython-38.pyc,, +jinja2/__pycache__/meta.cpython-38.pyc,, +jinja2/__pycache__/nativetypes.cpython-38.pyc,, +jinja2/__pycache__/nodes.cpython-38.pyc,, +jinja2/__pycache__/optimizer.cpython-38.pyc,, +jinja2/__pycache__/parser.cpython-38.pyc,, +jinja2/__pycache__/runtime.cpython-38.pyc,, +jinja2/__pycache__/sandbox.cpython-38.pyc,, +jinja2/__pycache__/tests.cpython-38.pyc,, +jinja2/__pycache__/utils.cpython-38.pyc,, +jinja2/__pycache__/visitor.cpython-38.pyc,, +jinja2/_identifier.py,sha256=EdgGJKi7O1yvr4yFlvqPNEqV6M1qHyQr8Gt8GmVTKVM,1775 +jinja2/async_utils.py,sha256=jBcJSmLoQa2PjJdNcOpwaUmBxFNE9rZNwMF7Ob3dP9I,1947 +jinja2/bccache.py,sha256=v5rKAlYxIvfJEa0uGzAC6yCYSS3KuXT5Eqi-n9qvNi8,12670 +jinja2/compiler.py,sha256=v7zKz-mgSYXmfXD9mRmi2BU0B6Z-1RGZmOXCrsPKzc0,72209 +jinja2/constants.py,sha256=GMoFydBF_kdpaRKPoM5cl5MviquVRLVyZtfp5-16jg0,1433 +jinja2/debug.py,sha256=r0JL0vfO7HPlyKZEdr6eVlg7HoIg2OQGmJ7SeUEyAeI,8494 +jinja2/defaults.py,sha256=boBcSw78h-lp20YbaXSJsqkAI2uN_mD_TtCydpeq5wU,1267 +jinja2/environment.py,sha256=Vz20npBX5-SUH_eguQuxrSQDEsLFjho0qcHLdMhY3hA,60983 +jinja2/exceptions.py,sha256=ioHeHrWwCWNaXX1inHmHVblvc4haO7AXsjCp3GfWvx0,5071 +jinja2/ext.py,sha256=44SjDjeYkkxQTpmC2BetOTxEFMgQ42p2dfSwXmPFcSo,32122 +jinja2/filters.py,sha256=jusKTZbd0ddZMaibZkxMUVKNsOsaYtOq_Il8Imtx4BE,52609 +jinja2/idtracking.py,sha256=WekexMql3u5n3vDxFsQ_i8HW0j24AtjWTjrPBLWrHww,10721 +jinja2/lexer.py,sha256=qNEQqDQw_zO5EaH6rFQsER7Qwn2du0o22prB-TR11HE,29930 +jinja2/loaders.py,sha256=1MjXJOU6p4VywFqtpDZhtvtT_vIlmHnZKMKHHw4SZzA,22754 +jinja2/meta.py,sha256=GNPEvifmSaU3CMxlbheBOZjeZ277HThOPUTf1RkppKQ,4396 +jinja2/nativetypes.py,sha256=KCJl71MogrDih_BHBu6xV5p7Cr_jggAgu-shKTg6L28,3969 +jinja2/nodes.py,sha256=i34GPRAZexXMT6bwuf5SEyvdmS-bRCy9KMjwN5O6pjk,34550 +jinja2/optimizer.py,sha256=tHkMwXxfZkbfA1KmLcqmBMSaz7RLIvvItrJcPoXTyD8,1650 +jinja2/parser.py,sha256=kHnU8v92GwMYkfr0MVakWv8UlSf_kJPx8LUsgQMof70,39767 +jinja2/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jinja2/runtime.py,sha256=wVRlkEmAgNU67AIQDqLvI6UkNLkzDqpLA-z4Mi3vl3g,35054 +jinja2/sandbox.py,sha256=-8zxR6TO9kUkciAVFsIKu8Oq-C7PTeYEdZ5TtA55-gw,14600 +jinja2/tests.py,sha256=Am5Z6Lmfr2XaH_npIfJJ8MdXtWsbLjMULZJulTAj30E,5905 +jinja2/utils.py,sha256=udQxWIKaq4QDCZiXN31ngKOaGGdaMA5fl0JMaM-F6fg,26971 +jinja2/visitor.py,sha256=ZmeLuTj66ic35-uFH-1m0EKXiw4ObDDb_WuE6h5vPFg,3572 diff --git a/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/WHEEL b/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/WHEEL new file mode 100644 index 000000000..5bad85fdc --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/entry_points.txt b/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/entry_points.txt new file mode 100644 index 000000000..3619483fd --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[babel.extractors] +jinja2 = jinja2.ext:babel_extract [i18n] + diff --git a/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/top_level.txt b/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/top_level.txt new file mode 100644 index 000000000..7f7afbf3b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Jinja2-3.0.3.dist-info/top_level.txt @@ -0,0 +1 @@ +jinja2 diff --git a/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/INSTALLER b/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/LICENSE.rst b/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/LICENSE.rst new file mode 100644 index 000000000..9d227a0cc --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/LICENSE.rst @@ -0,0 +1,28 @@ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/METADATA b/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/METADATA new file mode 100644 index 000000000..ef44e2b35 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/METADATA @@ -0,0 +1,100 @@ +Metadata-Version: 2.1 +Name: MarkupSafe +Version: 2.0.1 +Summary: Safely add untrusted strings to HTML/XML markup. +Home-page: https://palletsprojects.com/p/markupsafe/ +Author: Armin Ronacher +Author-email: armin.ronacher@active-4.com +Maintainer: Pallets +Maintainer-email: contact@palletsprojects.com +License: BSD-3-Clause +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Documentation, https://markupsafe.palletsprojects.com/ +Project-URL: Changes, https://markupsafe.palletsprojects.com/changes/ +Project-URL: Source Code, https://github.com/pallets/markupsafe/ +Project-URL: Issue Tracker, https://github.com/pallets/markupsafe/issues/ +Project-URL: Twitter, https://twitter.com/PalletsTeam +Project-URL: Chat, https://discord.gg/pallets +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Text Processing :: Markup :: HTML +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst + +MarkupSafe +========== + +MarkupSafe implements a text object that escapes characters so it is +safe to use in HTML and XML. Characters that have special meanings are +replaced so that they display as the actual characters. This mitigates +injection attacks, meaning untrusted user input can safely be displayed +on a page. + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + pip install -U MarkupSafe + +.. _pip: https://pip.pypa.io/en/stable/quickstart/ + + +Examples +-------- + +.. code-block:: pycon + + >>> from markupsafe import Markup, escape + + >>> # escape replaces special characters and wraps in Markup + >>> escape("") + Markup('<script>alert(document.cookie);</script>') + + >>> # wrap in Markup to mark text "safe" and prevent escaping + >>> Markup("Hello") + Markup('hello') + + >>> escape(Markup("Hello")) + Markup('hello') + + >>> # Markup is a str subclass + >>> # methods and operators escape their arguments + >>> template = Markup("Hello {name}") + >>> template.format(name='"World"') + Markup('Hello "World"') + + +Donate +------ + +The Pallets organization develops and supports MarkupSafe and other +popular packages. In order to grow the community of contributors and +users, and allow the maintainers to devote more time to the projects, +`please donate today`_. + +.. _please donate today: https://palletsprojects.com/donate + + +Links +----- + +- Documentation: https://markupsafe.palletsprojects.com/ +- Changes: https://markupsafe.palletsprojects.com/changes/ +- PyPI Releases: https://pypi.org/project/MarkupSafe/ +- Source Code: https://github.com/pallets/markupsafe/ +- Issue Tracker: https://github.com/pallets/markupsafe/issues/ +- Website: https://palletsprojects.com/p/markupsafe/ +- Twitter: https://twitter.com/PalletsTeam +- Chat: https://discord.gg/pallets + + diff --git a/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/RECORD b/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/RECORD new file mode 100644 index 000000000..274f15114 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/RECORD @@ -0,0 +1,14 @@ +MarkupSafe-2.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +MarkupSafe-2.0.1.dist-info/LICENSE.rst,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475 +MarkupSafe-2.0.1.dist-info/METADATA,sha256=FmPpxBdaqCCjF-XKqoxeEzqAzhetQnrkkSsd3V3X-Jc,3211 +MarkupSafe-2.0.1.dist-info/RECORD,, +MarkupSafe-2.0.1.dist-info/WHEEL,sha256=RA4ju32EWHpO-G1BhxTQ3AwXQWjnnrzYGkM9skoN7_I,109 +MarkupSafe-2.0.1.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11 +markupsafe/__init__.py,sha256=9Tez4UIlI7J6_sQcUFK1dKniT_b_8YefpGIyYJ3Sr2Q,8923 +markupsafe/__pycache__/__init__.cpython-38.pyc,, +markupsafe/__pycache__/_native.cpython-38.pyc,, +markupsafe/_native.py,sha256=GTKEV-bWgZuSjklhMHOYRHU9k0DMewTf5mVEZfkbuns,1986 +markupsafe/_speedups.c,sha256=CDDtwaV21D2nYtypnMQzxvvpZpcTvIs8OZ6KDa1g4t0,7400 +markupsafe/_speedups.cpython-38-darwin.so,sha256=Tjek-7TDXevv7qgNBsLLUfsOQlfCSCGzhhV5cCUrIxk,35480 +markupsafe/_speedups.pyi,sha256=vfMCsOgbAXRNLUXkyuyonG8uEWKYU4PDqNuMaDELAYw,229 +markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/WHEEL b/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/WHEEL new file mode 100644 index 000000000..d70ba8e3f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.36.2) +Root-Is-Purelib: false +Tag: cp38-cp38-macosx_10_9_x86_64 + diff --git a/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/top_level.txt b/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/top_level.txt new file mode 100644 index 000000000..75bf72925 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/MarkupSafe-2.0.1.dist-info/top_level.txt @@ -0,0 +1 @@ +markupsafe diff --git a/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/INSTALLER b/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/LICENSE b/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/LICENSE new file mode 100644 index 000000000..0d9fb6dc4 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/LICENSE @@ -0,0 +1,19 @@ +Copyright 2005-2021 SQLAlchemy authors and contributors . + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/METADATA b/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/METADATA new file mode 100644 index 000000000..cc9841916 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/METADATA @@ -0,0 +1,239 @@ +Metadata-Version: 2.1 +Name: SQLAlchemy +Version: 1.4.27 +Summary: Database Abstraction Library +Home-page: https://www.sqlalchemy.org +Author: Mike Bayer +Author-email: mike_mp@zzzcomputing.com +License: MIT +Project-URL: Documentation, https://docs.sqlalchemy.org +Project-URL: Issue Tracker, https://github.com/sqlalchemy/sqlalchemy/ +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Database :: Front-Ends +Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7 +Description-Content-Type: text/x-rst +Requires-Dist: importlib-metadata ; python_version < "3.8" +Requires-Dist: greenlet (!=0.4.17) ; python_version >= "3" and (platform_machine == "aarch64" or (platform_machine == "ppc64le" or (platform_machine == "x86_64" or (platform_machine == "amd64" or (platform_machine == "AMD64" or (platform_machine == "win32" or platform_machine == "WIN32")))))) +Provides-Extra: aiomysql +Requires-Dist: greenlet (!=0.4.17) ; (python_version >= "3") and extra == 'aiomysql' +Requires-Dist: aiomysql ; (python_version >= "3") and extra == 'aiomysql' +Provides-Extra: aiosqlite +Requires-Dist: typing-extensions (!=3.10.0.1) ; extra == 'aiosqlite' +Requires-Dist: greenlet (!=0.4.17) ; (python_version >= "3") and extra == 'aiosqlite' +Requires-Dist: aiosqlite ; (python_version >= "3") and extra == 'aiosqlite' +Provides-Extra: asyncio +Requires-Dist: greenlet (!=0.4.17) ; (python_version >= "3") and extra == 'asyncio' +Provides-Extra: asyncmy +Requires-Dist: greenlet (!=0.4.17) ; (python_version >= "3") and extra == 'asyncmy' +Requires-Dist: asyncmy (>=0.2.3) ; (python_version >= "3") and extra == 'asyncmy' +Provides-Extra: mariadb_connector +Requires-Dist: mariadb (>=1.0.1) ; (python_version >= "3") and extra == 'mariadb_connector' +Provides-Extra: mssql +Requires-Dist: pyodbc ; extra == 'mssql' +Provides-Extra: mssql_pymssql +Requires-Dist: pymssql ; extra == 'mssql_pymssql' +Provides-Extra: mssql_pyodbc +Requires-Dist: pyodbc ; extra == 'mssql_pyodbc' +Provides-Extra: mypy +Requires-Dist: sqlalchemy2-stubs ; extra == 'mypy' +Requires-Dist: mypy (>=0.910) ; (python_version >= "3") and extra == 'mypy' +Provides-Extra: mysql +Requires-Dist: mysqlclient (<2,>=1.4.0) ; (python_version < "3") and extra == 'mysql' +Requires-Dist: mysqlclient (>=1.4.0) ; (python_version >= "3") and extra == 'mysql' +Provides-Extra: mysql_connector +Requires-Dist: mysql-connector-python ; extra == 'mysql_connector' +Provides-Extra: oracle +Requires-Dist: cx-oracle (<8,>=7) ; (python_version < "3") and extra == 'oracle' +Requires-Dist: cx-oracle (>=7) ; (python_version >= "3") and extra == 'oracle' +Provides-Extra: postgresql +Requires-Dist: psycopg2 (>=2.7) ; extra == 'postgresql' +Provides-Extra: postgresql_asyncpg +Requires-Dist: greenlet (!=0.4.17) ; (python_version >= "3") and extra == 'postgresql_asyncpg' +Requires-Dist: asyncpg ; (python_version >= "3") and extra == 'postgresql_asyncpg' +Provides-Extra: postgresql_pg8000 +Requires-Dist: pg8000 (>=1.16.6) ; extra == 'postgresql_pg8000' +Provides-Extra: postgresql_psycopg2binary +Requires-Dist: psycopg2-binary ; extra == 'postgresql_psycopg2binary' +Provides-Extra: postgresql_psycopg2cffi +Requires-Dist: psycopg2cffi ; extra == 'postgresql_psycopg2cffi' +Provides-Extra: pymysql +Requires-Dist: pymysql (<1) ; (python_version < "3") and extra == 'pymysql' +Requires-Dist: pymysql ; (python_version >= "3") and extra == 'pymysql' +Provides-Extra: sqlcipher +Requires-Dist: sqlcipher3-binary ; (python_version >= "3") and extra == 'sqlcipher' + +SQLAlchemy +========== + +|PyPI| |Python| |Downloads| + +.. |PyPI| image:: https://img.shields.io/pypi/v/sqlalchemy + :target: https://pypi.org/project/sqlalchemy + :alt: PyPI + +.. |Python| image:: https://img.shields.io/pypi/pyversions/sqlalchemy + :target: https://pypi.org/project/sqlalchemy + :alt: PyPI - Python Version + +.. |Downloads| image:: https://img.shields.io/pypi/dm/sqlalchemy + :target: https://pypi.org/project/sqlalchemy + :alt: PyPI - Downloads + + +The Python SQL Toolkit and Object Relational Mapper + +Introduction +------------- + +SQLAlchemy is the Python SQL toolkit and Object Relational Mapper +that gives application developers the full power and +flexibility of SQL. SQLAlchemy provides a full suite +of well known enterprise-level persistence patterns, +designed for efficient and high-performing database +access, adapted into a simple and Pythonic domain +language. + +Major SQLAlchemy features include: + +* An industrial strength ORM, built + from the core on the identity map, unit of work, + and data mapper patterns. These patterns + allow transparent persistence of objects + using a declarative configuration system. + Domain models + can be constructed and manipulated naturally, + and changes are synchronized with the + current transaction automatically. +* A relationally-oriented query system, exposing + the full range of SQL's capabilities + explicitly, including joins, subqueries, + correlation, and most everything else, + in terms of the object model. + Writing queries with the ORM uses the same + techniques of relational composition you use + when writing SQL. While you can drop into + literal SQL at any time, it's virtually never + needed. +* A comprehensive and flexible system + of eager loading for related collections and objects. + Collections are cached within a session, + and can be loaded on individual access, all + at once using joins, or by query per collection + across the full result set. +* A Core SQL construction system and DBAPI + interaction layer. The SQLAlchemy Core is + separate from the ORM and is a full database + abstraction layer in its own right, and includes + an extensible Python-based SQL expression + language, schema metadata, connection pooling, + type coercion, and custom types. +* All primary and foreign key constraints are + assumed to be composite and natural. Surrogate + integer primary keys are of course still the + norm, but SQLAlchemy never assumes or hardcodes + to this model. +* Database introspection and generation. Database + schemas can be "reflected" in one step into + Python structures representing database metadata; + those same structures can then generate + CREATE statements right back out - all within + the Core, independent of the ORM. + +SQLAlchemy's philosophy: + +* SQL databases behave less and less like object + collections the more size and performance start to + matter; object collections behave less and less like + tables and rows the more abstraction starts to matter. + SQLAlchemy aims to accommodate both of these + principles. +* An ORM doesn't need to hide the "R". A relational + database provides rich, set-based functionality + that should be fully exposed. SQLAlchemy's + ORM provides an open-ended set of patterns + that allow a developer to construct a custom + mediation layer between a domain model and + a relational schema, turning the so-called + "object relational impedance" issue into + a distant memory. +* The developer, in all cases, makes all decisions + regarding the design, structure, and naming conventions + of both the object model as well as the relational + schema. SQLAlchemy only provides the means + to automate the execution of these decisions. +* With SQLAlchemy, there's no such thing as + "the ORM generated a bad query" - you + retain full control over the structure of + queries, including how joins are organized, + how subqueries and correlation is used, what + columns are requested. Everything SQLAlchemy + does is ultimately the result of a developer- + initiated decision. +* Don't use an ORM if the problem doesn't need one. + SQLAlchemy consists of a Core and separate ORM + component. The Core offers a full SQL expression + language that allows Pythonic construction + of SQL constructs that render directly to SQL + strings for a target database, returning + result sets that are essentially enhanced DBAPI + cursors. +* Transactions should be the norm. With SQLAlchemy's + ORM, nothing goes to permanent storage until + commit() is called. SQLAlchemy encourages applications + to create a consistent means of delineating + the start and end of a series of operations. +* Never render a literal value in a SQL statement. + Bound parameters are used to the greatest degree + possible, allowing query optimizers to cache + query plans effectively and making SQL injection + attacks a non-issue. + +Documentation +------------- + +Latest documentation is at: + +https://www.sqlalchemy.org/docs/ + +Installation / Requirements +--------------------------- + +Full documentation for installation is at +`Installation `_. + +Getting Help / Development / Bug reporting +------------------------------------------ + +Please refer to the `SQLAlchemy Community Guide `_. + +Code of Conduct +--------------- + +Above all, SQLAlchemy places great emphasis on polite, thoughtful, and +constructive communication between users and developers. +Please see our current Code of Conduct at +`Code of Conduct `_. + +License +------- + +SQLAlchemy is distributed under the `MIT license +`_. + + + diff --git a/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/RECORD b/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/RECORD new file mode 100644 index 000000000..3d89d46a1 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/RECORD @@ -0,0 +1,485 @@ +SQLAlchemy-1.4.27.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +SQLAlchemy-1.4.27.dist-info/LICENSE,sha256=_-DCK5JvsC0ovMsgocueJWTu1m_PSeTv7r8oHE-pf6c,1100 +SQLAlchemy-1.4.27.dist-info/METADATA,sha256=r_0ZuzMG-zLC1Xu6xgYd0saZ0yHW8gXPE_9XxjvII-A,9945 +SQLAlchemy-1.4.27.dist-info/RECORD,, +SQLAlchemy-1.4.27.dist-info/WHEEL,sha256=Hxr7O-X8T9h9kBRUEXrRY-j217oAickuD6_DXvSLvAU,110 +SQLAlchemy-1.4.27.dist-info/top_level.txt,sha256=rp-ZgB7D8G11ivXON5VGPjupT1voYmWqkciDt5Uaw_Q,11 +sqlalchemy/__init__.py,sha256=rKNVSFLbGkYhTRpfm7QFXZoij92dwPx-736en34wrqM,4114 +sqlalchemy/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/__pycache__/events.cpython-38.pyc,, +sqlalchemy/__pycache__/exc.cpython-38.pyc,, +sqlalchemy/__pycache__/inspection.cpython-38.pyc,, +sqlalchemy/__pycache__/log.cpython-38.pyc,, +sqlalchemy/__pycache__/processors.cpython-38.pyc,, +sqlalchemy/__pycache__/schema.cpython-38.pyc,, +sqlalchemy/__pycache__/types.cpython-38.pyc,, +sqlalchemy/cimmutabledict.cpython-38-darwin.so,sha256=zdATsu1CZoQvAheaGD3STSGWEcQcY6ReCinQDb3PDzw,37864 +sqlalchemy/connectors/__init__.py,sha256=O443ri6SrKVeRqNLMyqjX0DHFvuPxo9AdZIDkodhxwA,279 +sqlalchemy/connectors/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/connectors/__pycache__/mxodbc.cpython-38.pyc,, +sqlalchemy/connectors/__pycache__/pyodbc.cpython-38.pyc,, +sqlalchemy/connectors/mxodbc.py,sha256=EbSWZRvQFw2Ng0ec9MN4KtLJvOuTPw4lSYglho5rYL8,5784 +sqlalchemy/connectors/pyodbc.py,sha256=qVLG7itujednjC-rPVn7VWW07Mou7dDBJmNQdUhTXtk,6825 +sqlalchemy/cprocessors.cpython-38-darwin.so,sha256=z9-KZfQzsIUsW_qrayRkWNNIP7MSYA9wBgY-bLHmxuI,37272 +sqlalchemy/cresultproxy.cpython-38-darwin.so,sha256=YFfY6cpwnt_W_n7ZzD14jdtt97fHQI72AeAp7UQic3Q,41376 +sqlalchemy/databases/__init__.py,sha256=vGQM3BYXHXy6RBTFNiL80biiW3fn-LoguUjJKiFnStE,1010 +sqlalchemy/databases/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/dialects/__init__.py,sha256=66uS-lZx94aGVQvEqy_z8m1pC0P3cI-CKEWCIL2Xlsk,2085 +sqlalchemy/dialects/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/dialects/firebird/__init__.py,sha256=wZ9npV8FYElLZEYmrP1ksvN90_6YR1RkIHnT6rjxhfs,1153 +sqlalchemy/dialects/firebird/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/dialects/firebird/__pycache__/base.cpython-38.pyc,, +sqlalchemy/dialects/firebird/__pycache__/fdb.cpython-38.pyc,, +sqlalchemy/dialects/firebird/__pycache__/kinterbasdb.cpython-38.pyc,, +sqlalchemy/dialects/firebird/base.py,sha256=wUBiQwvIf35OdNUfU_Vi_rtGYeIdM7DUKogfh0KYzRY,31171 +sqlalchemy/dialects/firebird/fdb.py,sha256=w4Kc-IubUKZgY5yTcgewvzZcU2WOnuwXM94dePqbEmk,4116 +sqlalchemy/dialects/firebird/kinterbasdb.py,sha256=dQbCC8vGifRyeQhekSc_t0Zj5XKFpTH0C2XyBcxRun0,6479 +sqlalchemy/dialects/mssql/__init__.py,sha256=-tzu6QvNJpSsrB_OqbqA1WnHGJdTiFe0LRRFEBL8qiY,1788 +sqlalchemy/dialects/mssql/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/dialects/mssql/__pycache__/base.cpython-38.pyc,, +sqlalchemy/dialects/mssql/__pycache__/information_schema.cpython-38.pyc,, +sqlalchemy/dialects/mssql/__pycache__/json.cpython-38.pyc,, +sqlalchemy/dialects/mssql/__pycache__/mxodbc.cpython-38.pyc,, +sqlalchemy/dialects/mssql/__pycache__/provision.cpython-38.pyc,, +sqlalchemy/dialects/mssql/__pycache__/pymssql.cpython-38.pyc,, +sqlalchemy/dialects/mssql/__pycache__/pyodbc.cpython-38.pyc,, +sqlalchemy/dialects/mssql/base.py,sha256=6Z7fEN1eCczHcI1xrlWHGV1Km3VFQIc-hdgi75IoU60,114380 +sqlalchemy/dialects/mssql/information_schema.py,sha256=MeIFJ0gK1Um0jE1S0rG5q3cv3Mk6N_PftPUR0h7F7qU,7584 +sqlalchemy/dialects/mssql/json.py,sha256=K1RqVl5bslYyVMtk5CWGjRV_I4K1sszXjx2F_nbCVWI,4558 +sqlalchemy/dialects/mssql/mxodbc.py,sha256=QHeIbRAlNxM47dNkTaly1Qvhjoc627YsF-iTKuL_goY,4808 +sqlalchemy/dialects/mssql/provision.py,sha256=m7ofLZYZinDS91Vgs42fK7dhJNnH-J_Bw2x_tP59tCc,4255 +sqlalchemy/dialects/mssql/pymssql.py,sha256=xBkFqpSSZ2cCn4Cop6NuV4ZBN_ARVyZzh_HKpkNIRLY,4843 +sqlalchemy/dialects/mssql/pyodbc.py,sha256=Rv-J3jEB9G6p7eNnzB0boSzYuU36QEyLeq9bmc4zhpQ,21965 +sqlalchemy/dialects/mysql/__init__.py,sha256=2yMggm7oNcGHDrwBSBd2x7JRaYaBXl8hRHZKjW3tnuQ,2190 +sqlalchemy/dialects/mysql/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/aiomysql.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/asyncmy.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/base.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/cymysql.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/dml.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/enumerated.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/expression.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/json.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/mariadb.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/mariadbconnector.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/mysqlconnector.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/mysqldb.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/oursql.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/provision.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/pymysql.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/pyodbc.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/reflection.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/reserved_words.cpython-38.pyc,, +sqlalchemy/dialects/mysql/__pycache__/types.cpython-38.pyc,, +sqlalchemy/dialects/mysql/aiomysql.py,sha256=JO7VVQneB93f48DIfsu63PJW-OHaQ3m3Wd-4YPpdSSE,9609 +sqlalchemy/dialects/mysql/asyncmy.py,sha256=4tiiRm3l0ncgGwf5WzVik6z-IPD-ZcLEEJx4swMy_N0,9906 +sqlalchemy/dialects/mysql/base.py,sha256=mVnY2O7onyIRRoJEeeobNRVQclAx5ok3gl_xYO3SG5A,114150 +sqlalchemy/dialects/mysql/cymysql.py,sha256=MN5fsHApPDQxDHRPWHqSm6vznMWgxCJOtg4vEHuaMYs,2271 +sqlalchemy/dialects/mysql/dml.py,sha256=rCXGbiKl8iMi7AS30_turHWeouusLGSpSLtdHKnoUl4,6182 +sqlalchemy/dialects/mysql/enumerated.py,sha256=ZIy-3XQZtERuYv3jhvlnz5S_DCf-oiHACdgf2ymwEm4,9143 +sqlalchemy/dialects/mysql/expression.py,sha256=HJ4IO3LPJk4cQYIL-O-jN2vLWxVGCqem_K3h8kKNWzE,3741 +sqlalchemy/dialects/mysql/json.py,sha256=JWBHb0QmE9w47gsqZyfmUQpdi8GePHutGVJQVvixigg,2313 +sqlalchemy/dialects/mysql/mariadb.py,sha256=OBwN9RMQLP-xqLbNMAe5uoz7PEtqa68ln2HwwA6KUn8,585 +sqlalchemy/dialects/mysql/mariadbconnector.py,sha256=7FUPec_smSEl9omC_iQJx7Ywmz7Q-J_EFgfFklzNBz8,7337 +sqlalchemy/dialects/mysql/mysqlconnector.py,sha256=W8CL7P7hnZhmN3Pl6nwsOhwSMYvHWfCrlrWtx2F3zpU,7690 +sqlalchemy/dialects/mysql/mysqldb.py,sha256=k_mWzeIhlPK_Tcear93uKgoAZOX9Qfna7ls3u2KkSiw,10437 +sqlalchemy/dialects/mysql/oursql.py,sha256=m0lhnKgGli4u_DZdsFwgz0d2iXklB4rHfacj_QfM2Tw,8523 +sqlalchemy/dialects/mysql/provision.py,sha256=P5ma4Xy5eSOFIcMjIe_zAwu_6ncSXSLVZYYSMS5Io9c,2649 +sqlalchemy/dialects/mysql/pymysql.py,sha256=n9bgdO54bO1Dp8xS61PMpKoo1RUkPngwrlxBLX0Ovts,2770 +sqlalchemy/dialects/mysql/pyodbc.py,sha256=6XXmo7LluP1IfVe3dznOiC3wSH76q-tBpCSf2L9mS7w,4498 +sqlalchemy/dialects/mysql/reflection.py,sha256=eQpTm4N5swlAginvFhbOEiuVHFVdxcPAbAwMkzcS4n8,18553 +sqlalchemy/dialects/mysql/reserved_words.py,sha256=p9tEfMmf2qwB5mkkXQjARdtO9WoB4YYWpjYmHLXYVd0,9104 +sqlalchemy/dialects/mysql/types.py,sha256=x4SwOFKDmXmZbq88b6-dcHgo4CJzl8Np_gOsV8-0QZQ,24589 +sqlalchemy/dialects/oracle/__init__.py,sha256=a2cdiwS50KoRc0-3PrfFeTHwaw-aTb3NzGB0E60TmG8,1229 +sqlalchemy/dialects/oracle/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/dialects/oracle/__pycache__/base.cpython-38.pyc,, +sqlalchemy/dialects/oracle/__pycache__/cx_oracle.cpython-38.pyc,, +sqlalchemy/dialects/oracle/__pycache__/provision.cpython-38.pyc,, +sqlalchemy/dialects/oracle/base.py,sha256=Qy_X7KNsj5pNHEsb_daViUOxDqoTYaTW59X2NRFaRU0,86866 +sqlalchemy/dialects/oracle/cx_oracle.py,sha256=24c3oUQCylpuerqWdHEzvRq3MyIclKcAiukiUTUPM9w,52734 +sqlalchemy/dialects/oracle/provision.py,sha256=enaF61XI53b92R5LBUt1CPOLUMBWI7Ulktiqs7z54Yg,5805 +sqlalchemy/dialects/postgresql/__init__.py,sha256=mpE2L4a0CMcnYHcMBGpZfm3fTpzmTMqa3NHTtQtdGTE,2509 +sqlalchemy/dialects/postgresql/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/dialects/postgresql/__pycache__/array.cpython-38.pyc,, +sqlalchemy/dialects/postgresql/__pycache__/asyncpg.cpython-38.pyc,, +sqlalchemy/dialects/postgresql/__pycache__/base.cpython-38.pyc,, +sqlalchemy/dialects/postgresql/__pycache__/dml.cpython-38.pyc,, +sqlalchemy/dialects/postgresql/__pycache__/ext.cpython-38.pyc,, +sqlalchemy/dialects/postgresql/__pycache__/hstore.cpython-38.pyc,, +sqlalchemy/dialects/postgresql/__pycache__/json.cpython-38.pyc,, +sqlalchemy/dialects/postgresql/__pycache__/pg8000.cpython-38.pyc,, +sqlalchemy/dialects/postgresql/__pycache__/provision.cpython-38.pyc,, +sqlalchemy/dialects/postgresql/__pycache__/psycopg2.cpython-38.pyc,, +sqlalchemy/dialects/postgresql/__pycache__/psycopg2cffi.cpython-38.pyc,, +sqlalchemy/dialects/postgresql/__pycache__/pygresql.cpython-38.pyc,, +sqlalchemy/dialects/postgresql/__pycache__/pypostgresql.cpython-38.pyc,, +sqlalchemy/dialects/postgresql/__pycache__/ranges.cpython-38.pyc,, +sqlalchemy/dialects/postgresql/array.py,sha256=AiOblctZe0Xu4nZP7kjqQWRkDjj0vuLmBmv-X-Pc3yg,12233 +sqlalchemy/dialects/postgresql/asyncpg.py,sha256=_YoJRX2llfKD2_hgXhDOSwtn4bAdB__U3gqHZFa_Jr8,34265 +sqlalchemy/dialects/postgresql/base.py,sha256=y--qcxDAR9PtV4bMxGKwWPZvFFxIyqGLwTpx1GtnUMs,153874 +sqlalchemy/dialects/postgresql/dml.py,sha256=6fsyXbbISrWCNq4tLs-hcLpXObtv7-xcWmuNR7pSUy8,9556 +sqlalchemy/dialects/postgresql/ext.py,sha256=pcX7pfWbzIoPOFLhXFqUrSYHXVbYAAoFZ9VLg-mE4aQ,8383 +sqlalchemy/dialects/postgresql/hstore.py,sha256=mh3VhuJa8_utkAXr-ZQNMZC9c-WK8SPD43M-2OJCudE,12496 +sqlalchemy/dialects/postgresql/json.py,sha256=bK-uBXv8r4ewZqUSoF-5JGzA2JcLAwJV6vkJDZ9OTMU,10556 +sqlalchemy/dialects/postgresql/pg8000.py,sha256=QhJevnFnjmK0JUu_UdeCfNjS2OTojX48pOfmrumr5JU,17044 +sqlalchemy/dialects/postgresql/provision.py,sha256=2hQBww2CBUz47MafjSVCVeCMoRWcCEG9fiTsKCm5KHk,4416 +sqlalchemy/dialects/postgresql/psycopg2.py,sha256=H4zoPsOjb5G76HZXU_eJyuU4QGeM8MzUWGLS7UAIjRA,38308 +sqlalchemy/dialects/postgresql/psycopg2cffi.py,sha256=lls7ZpikR4KvOQk2Nbh8z5IAT2Zu1D1Y280Mq4WQpOY,1691 +sqlalchemy/dialects/postgresql/pygresql.py,sha256=1qfzOFvEUExkvAiiFkLnVVHg6vfXGLzyp41UzBwKf24,8585 +sqlalchemy/dialects/postgresql/pypostgresql.py,sha256=3xY2pwLeYOBR7BCpj2pTtGtwcwS0VDDxM2GFmFogNPU,3693 +sqlalchemy/dialects/postgresql/ranges.py,sha256=Bfa9dLkWM51P-W_oAcnvbpUzsMg4ZrBWTN3ppi-n3Yc,4731 +sqlalchemy/dialects/sqlite/__init__.py,sha256=YQ5ryj0ZDE_3s559hsnm2cxuX2mI3ebEJ9Mf-DLmMA8,1198 +sqlalchemy/dialects/sqlite/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/dialects/sqlite/__pycache__/aiosqlite.cpython-38.pyc,, +sqlalchemy/dialects/sqlite/__pycache__/base.cpython-38.pyc,, +sqlalchemy/dialects/sqlite/__pycache__/dml.cpython-38.pyc,, +sqlalchemy/dialects/sqlite/__pycache__/json.cpython-38.pyc,, +sqlalchemy/dialects/sqlite/__pycache__/provision.cpython-38.pyc,, +sqlalchemy/dialects/sqlite/__pycache__/pysqlcipher.cpython-38.pyc,, +sqlalchemy/dialects/sqlite/__pycache__/pysqlite.cpython-38.pyc,, +sqlalchemy/dialects/sqlite/aiosqlite.py,sha256=XBpRrOb8zD5tMmZrdLxZ3MlQKG35JfcK7Muel8Z5Yzo,9995 +sqlalchemy/dialects/sqlite/base.py,sha256=h41saLgOroxbo1wLRjkc-x7m-N1bec6KKXrXIz_s9rA,88332 +sqlalchemy/dialects/sqlite/dml.py,sha256=A60UFadXJ7erPfg6xghfPrgQwCRcOYa1EUVxmdmdd04,6839 +sqlalchemy/dialects/sqlite/json.py,sha256=oFw4Rt8xw-tkD3IMlm3TDEGe1RqrTyvIuqjABsxn8EI,2518 +sqlalchemy/dialects/sqlite/provision.py,sha256=AQILXN5PBUSM05c-SFSFFhPdFqcQDwdoKtUnvLDac14,4676 +sqlalchemy/dialects/sqlite/pysqlcipher.py,sha256=oO4myPd2OOf8ACKlyofjEV95PonyFF3l6jdXezLh9Tw,5605 +sqlalchemy/dialects/sqlite/pysqlite.py,sha256=QfvICDp6cW4myH8eMOlygSKkwv7nlfgEc5yHdB1qL4o,23441 +sqlalchemy/dialects/sybase/__init__.py,sha256=_MpFLF4UYNG1YWxabCKP5p5_a2T8aYQdGb4zOgisSjE,1364 +sqlalchemy/dialects/sybase/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/dialects/sybase/__pycache__/base.cpython-38.pyc,, +sqlalchemy/dialects/sybase/__pycache__/mxodbc.cpython-38.pyc,, +sqlalchemy/dialects/sybase/__pycache__/pyodbc.cpython-38.pyc,, +sqlalchemy/dialects/sybase/__pycache__/pysybase.cpython-38.pyc,, +sqlalchemy/dialects/sybase/base.py,sha256=eTB8YYJm5iN-52e3X89zSfXGk_b6TVjbrBMSJgjrYwU,32421 +sqlalchemy/dialects/sybase/mxodbc.py,sha256=Y3ws2Ahe8yzcnzYeclQmujCgmIMK4Lm4tWtAFmo2IaI,939 +sqlalchemy/dialects/sybase/pyodbc.py,sha256=XV9fGWBFKSalUlNtWhRqnfdPzGknrt6Yr4D8yeRRV1E,2230 +sqlalchemy/dialects/sybase/pysybase.py,sha256=8V3fvp1R52o1DLzri8kZ5LLkXp68knyJ6CkwI_mIHoo,3370 +sqlalchemy/engine/__init__.py,sha256=ZHMG_4TdVFEzXuvOAKx_yEtmVMZHnfOyUd3XGQLFivU,2075 +sqlalchemy/engine/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/engine/__pycache__/base.cpython-38.pyc,, +sqlalchemy/engine/__pycache__/characteristics.cpython-38.pyc,, +sqlalchemy/engine/__pycache__/create.cpython-38.pyc,, +sqlalchemy/engine/__pycache__/cursor.cpython-38.pyc,, +sqlalchemy/engine/__pycache__/default.cpython-38.pyc,, +sqlalchemy/engine/__pycache__/events.cpython-38.pyc,, +sqlalchemy/engine/__pycache__/interfaces.cpython-38.pyc,, +sqlalchemy/engine/__pycache__/mock.cpython-38.pyc,, +sqlalchemy/engine/__pycache__/reflection.cpython-38.pyc,, +sqlalchemy/engine/__pycache__/result.cpython-38.pyc,, +sqlalchemy/engine/__pycache__/row.cpython-38.pyc,, +sqlalchemy/engine/__pycache__/strategies.cpython-38.pyc,, +sqlalchemy/engine/__pycache__/url.cpython-38.pyc,, +sqlalchemy/engine/__pycache__/util.cpython-38.pyc,, +sqlalchemy/engine/base.py,sha256=EmbRi24NQzwQkcKBmtrCc1PGD0FeQhWp4AScExrOS1k,119154 +sqlalchemy/engine/characteristics.py,sha256=qvd3T8HW470kIxN-x6OzycfjCFdnmbzcaFQeds7KHOw,1817 +sqlalchemy/engine/create.py,sha256=fYuff7D1cxS-dDg4_NyUHKaP66ckCrstsxC4pPkkTA4,31286 +sqlalchemy/engine/cursor.py,sha256=AoHt8Cg3NnC6aoxz19dw9thSxDAD5gObZRumCFBfOYM,68088 +sqlalchemy/engine/default.py,sha256=dG8lcErMCX58XoGLu30PQ5NJ-vNQB13hxaEVYa300vQ,64692 +sqlalchemy/engine/events.py,sha256=mnGuGKbF4zAxgSTVc0NHfGQvn7Vpp47pslGION-AaAU,33166 +sqlalchemy/engine/interfaces.py,sha256=ElnlzlTWNZKZaTQdQvwOff_U3Ao9tvNGssmnu0Nn5DM,60162 +sqlalchemy/engine/mock.py,sha256=37RtWX1xT7K1rem2jUtrKSynAb6HGxaq1YTW5iHAiSk,3626 +sqlalchemy/engine/reflection.py,sha256=WbFLy7C9WaPwlTVzAqtKzPcOBVOOUM251y06F5iIhkQ,38703 +sqlalchemy/engine/result.py,sha256=5Kwnd4pfrER9P5-20YCn4wc1Jg_JyLVkZv8JAADUyvU,55999 +sqlalchemy/engine/row.py,sha256=uHCGnP2Buf80pQvM45-uE5znJetdVMKtjs2u2fzaXls,18191 +sqlalchemy/engine/strategies.py,sha256=mfpCWvmkLxZUW6TkJriTqCOJF7VCgDZXgzaqLsxauBc,414 +sqlalchemy/engine/url.py,sha256=kzQFzB5ecS2-940PCO4OfL_76MsjnZoNrEmEO5cxsCQ,26298 +sqlalchemy/engine/util.py,sha256=wFS1ZCg9AWlwDqc8zcOXYVjptwyqITRLOlgp6b1Sagw,7719 +sqlalchemy/event/__init__.py,sha256=1QT0XxdMGwMMdoLzx58dUn4HqKNvzEysfKkPOluQECY,517 +sqlalchemy/event/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/event/__pycache__/api.cpython-38.pyc,, +sqlalchemy/event/__pycache__/attr.cpython-38.pyc,, +sqlalchemy/event/__pycache__/base.cpython-38.pyc,, +sqlalchemy/event/__pycache__/legacy.cpython-38.pyc,, +sqlalchemy/event/__pycache__/registry.cpython-38.pyc,, +sqlalchemy/event/api.py,sha256=yvpDdd2Yefw4B1enmW1rNO6Z_aW-YhpsswhnEl5KuUw,8043 +sqlalchemy/event/attr.py,sha256=gXcuUY3EaoWjCq2Q5Keg0O_yjmI_FvxlaCUL6ko7JgA,14625 +sqlalchemy/event/base.py,sha256=i5ud1V77ViLUQJIO_-ENEbK1VEM8lkhqmRcXrk7rZJQ,10936 +sqlalchemy/event/legacy.py,sha256=kt_rKWVIHSPQvlRSkw4NwgLf5Oz7xahXaaW-OYbmB4g,6270 +sqlalchemy/event/registry.py,sha256=pCfpcG80P6C3m-iQReVNNTc_OKQllM1CL0AAtUl_CcU,8486 +sqlalchemy/events.py,sha256=SFtMYfSRcdOmXAUvLZ_KoQfA5bHGxLW-YnaCL2xILlM,467 +sqlalchemy/exc.py,sha256=LH9UIgl1XRI8DijZI620eD88ZnZFPHQ3Ed7D4uIfRmg,21116 +sqlalchemy/ext/__init__.py,sha256=3eg5n6pdQubMMU1UzaNNRpSxb8e3B4fAuhpmQ3v4kx4,322 +sqlalchemy/ext/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/ext/__pycache__/associationproxy.cpython-38.pyc,, +sqlalchemy/ext/__pycache__/automap.cpython-38.pyc,, +sqlalchemy/ext/__pycache__/baked.cpython-38.pyc,, +sqlalchemy/ext/__pycache__/compiler.cpython-38.pyc,, +sqlalchemy/ext/__pycache__/horizontal_shard.cpython-38.pyc,, +sqlalchemy/ext/__pycache__/hybrid.cpython-38.pyc,, +sqlalchemy/ext/__pycache__/indexable.cpython-38.pyc,, +sqlalchemy/ext/__pycache__/instrumentation.cpython-38.pyc,, +sqlalchemy/ext/__pycache__/mutable.cpython-38.pyc,, +sqlalchemy/ext/__pycache__/orderinglist.cpython-38.pyc,, +sqlalchemy/ext/__pycache__/serializer.cpython-38.pyc,, +sqlalchemy/ext/associationproxy.py,sha256=PmtfYj17pDhj0XNZTzzIiL2RBjc6ipgsypcuVjNnkAs,50568 +sqlalchemy/ext/asyncio/__init__.py,sha256=YzmnHWOudsK1IMLNP-eCMqEkL3jvaXQRrslGczH22-4,778 +sqlalchemy/ext/asyncio/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/ext/asyncio/__pycache__/base.cpython-38.pyc,, +sqlalchemy/ext/asyncio/__pycache__/engine.cpython-38.pyc,, +sqlalchemy/ext/asyncio/__pycache__/events.cpython-38.pyc,, +sqlalchemy/ext/asyncio/__pycache__/exc.cpython-38.pyc,, +sqlalchemy/ext/asyncio/__pycache__/result.cpython-38.pyc,, +sqlalchemy/ext/asyncio/__pycache__/scoping.cpython-38.pyc,, +sqlalchemy/ext/asyncio/__pycache__/session.cpython-38.pyc,, +sqlalchemy/ext/asyncio/base.py,sha256=VQmIq-CMEVQpZPMEa0K91tMxZMqKyCCAwJVuCLiG34w,2280 +sqlalchemy/ext/asyncio/engine.py,sha256=FVHbGqs8PlfxHHnuUM44hsARUjH4v6BqXm44M1oF4HE,26154 +sqlalchemy/ext/asyncio/events.py,sha256=616mp5MMyCF4OiOAp794L0tGMKmp-mTtbwukTqQ3-bo,1235 +sqlalchemy/ext/asyncio/exc.py,sha256=DwS55QWrcgoThCC3w-kE5xVnl8kUAiWm1NVcyuO0Ufs,639 +sqlalchemy/ext/asyncio/result.py,sha256=eE5nM4fzGcKD0FKg6qVnbSa8RmUKejJ96rAo7kVqdeA,20438 +sqlalchemy/ext/asyncio/scoping.py,sha256=KvNCLokPgm8MOoyPeEpF-WctrJVynp2gibNFAUNj3pE,2937 +sqlalchemy/ext/asyncio/session.py,sha256=q-1sMnuCNLGZrAiGXZfqpC13D3tUCAL_wYP5af-zZOc,19853 +sqlalchemy/ext/automap.py,sha256=JJqJDyPp9p7sl4htcDG_RWBdPAE6SOfxTlGFO0bAcFU,45195 +sqlalchemy/ext/baked.py,sha256=OzOdFF9Wvz9sflF2EYlIEHP9tKbVn3x8K6pEEgM4Kg4,19969 +sqlalchemy/ext/compiler.py,sha256=XnPSC8_mQTSYTXOSegt0-XpPxZXzJHyTCpQvdVG-WtE,17989 +sqlalchemy/ext/declarative/__init__.py,sha256=M4hGt8MVZzjVespP-G_3lUP1oCk9rev_xN5AjSgB6TU,1842 +sqlalchemy/ext/declarative/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/ext/declarative/__pycache__/extensions.cpython-38.pyc,, +sqlalchemy/ext/declarative/extensions.py,sha256=bNylndJ-MdWBC1gn5dS5WUzgfvsDT-0r1Gqfl6EUAJI,16409 +sqlalchemy/ext/horizontal_shard.py,sha256=KuqRl1GStQmcAfJ2bFf08kbV8Dktx1jYZ_ogf_FZAkI,8922 +sqlalchemy/ext/hybrid.py,sha256=UYrun9R3vzShnkcX43-8jVRyqOWF6PcqMICF_ebbbMs,41787 +sqlalchemy/ext/indexable.py,sha256=mOjILC84bSHxehal-E893YJLEELTYPz7MD8DHIRFCr4,11255 +sqlalchemy/ext/instrumentation.py,sha256=ReSLFxqbHgwAKNwoQQmKHoqYvWCob_WuXlPAEUJk4pk,14386 +sqlalchemy/ext/mutable.py,sha256=3ZfxmQoctFchZNGJppg-bzxPPSLvLcGKt_k6AQgDTXI,31997 +sqlalchemy/ext/mypy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sqlalchemy/ext/mypy/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/ext/mypy/__pycache__/apply.cpython-38.pyc,, +sqlalchemy/ext/mypy/__pycache__/decl_class.cpython-38.pyc,, +sqlalchemy/ext/mypy/__pycache__/infer.cpython-38.pyc,, +sqlalchemy/ext/mypy/__pycache__/names.cpython-38.pyc,, +sqlalchemy/ext/mypy/__pycache__/plugin.cpython-38.pyc,, +sqlalchemy/ext/mypy/__pycache__/util.cpython-38.pyc,, +sqlalchemy/ext/mypy/apply.py,sha256=z42HLeqy5Hh9v--2ohql16fa5Lbsas0UZMYeraovq6w,9503 +sqlalchemy/ext/mypy/decl_class.py,sha256=D_12pRqsO5lLw_FovRrDAykbeEirv4l2wuWLMHyWA1c,16698 +sqlalchemy/ext/mypy/infer.py,sha256=s1GqaZSbjXsnS4fdMDuPRQZ3tIjSwj4Zz_xhvYOqNH0,17742 +sqlalchemy/ext/mypy/names.py,sha256=CZjTn0YTsR-XN7gMBD-D39PolKQsDQCwcjk9loAoM5M,7687 +sqlalchemy/ext/mypy/plugin.py,sha256=HTyHlZeSzcUMT86TTpcjWOsXCe128k1ZJoLIPgU56qU,9223 +sqlalchemy/ext/mypy/util.py,sha256=z1z8UewpjK7xC9Kl8wFooikNn7xbQiMrYJHkTTwxJog,8149 +sqlalchemy/ext/orderinglist.py,sha256=pAhYXNDVm0o0ZuxtbmGFan3Fw8zhpJhiRPmrndzM-_8,13875 +sqlalchemy/ext/serializer.py,sha256=i2HZTt9O-PxidEXZKb9iDqJO3F0uhQ4w6Ens48wM6gY,5956 +sqlalchemy/future/__init__.py,sha256=b1swUP9MZmoZx3VXv6aQ2L9JB5iThBQe09SviZP8HYo,525 +sqlalchemy/future/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/future/__pycache__/engine.cpython-38.pyc,, +sqlalchemy/future/engine.py,sha256=Ly-M3NGamVrpnA9XOG_nVLra5f7OlmTMmg7dMb2tn4s,16184 +sqlalchemy/future/orm/__init__.py,sha256=Fj72ozD2mgP9R9t8E6vWarr5USz_5AUx7BqWLEld84w,289 +sqlalchemy/future/orm/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/inspection.py,sha256=EqHUnvpjuwiPUIdD92autVgiO2YAgC-yX9Trk1m5oSA,3051 +sqlalchemy/log.py,sha256=G-jGx-_08ZUS2J3djgTgt-coqb4fngSl6ehYaF7nmYE,6770 +sqlalchemy/orm/__init__.py,sha256=XrV7_csV7aCMCHFmRDfWgMjZmuoLOnACuL1FFWdLFAo,10714 +sqlalchemy/orm/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/attributes.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/base.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/clsregistry.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/collections.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/context.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/decl_api.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/decl_base.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/dependency.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/descriptor_props.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/dynamic.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/evaluator.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/events.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/exc.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/identity.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/instrumentation.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/interfaces.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/loading.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/mapper.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/path_registry.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/persistence.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/properties.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/query.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/relationships.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/scoping.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/session.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/state.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/strategies.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/strategy_options.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/sync.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/unitofwork.cpython-38.pyc,, +sqlalchemy/orm/__pycache__/util.cpython-38.pyc,, +sqlalchemy/orm/attributes.py,sha256=-wzQhvx7ebUXcTWLlSo5yNNIXlVYEO-v3UJSrNF07ow,76590 +sqlalchemy/orm/base.py,sha256=Nuo2suXVjo_GPl7p5alHe9o5862VtXQ7Mv2IEx2r-fQ,15068 +sqlalchemy/orm/clsregistry.py,sha256=POxg7BYwk8FkuMHPi8dmgBpCGSY3RzMdOm0iHJBLAcY,13291 +sqlalchemy/orm/collections.py,sha256=b6wKDA6IKCkItw6gKOmFfjXv7-UUrPXi8UaIWb8S2M4,54711 +sqlalchemy/orm/context.py,sha256=kBlYs-1xchSwKewTcQ_1j3lpR1S1y2ycDp77eIr9-1o,106495 +sqlalchemy/orm/decl_api.py,sha256=_eLE7TI3omhAKv_X_4nHZzkdFnntlpNLLMczBeKKcz8,34976 +sqlalchemy/orm/decl_base.py,sha256=aG4x39xwWl8mQmDBMlAyhdhGBbX0GF2ylSiML7Z1BRY,43322 +sqlalchemy/orm/dependency.py,sha256=lLqfIMcBWk4ot9qhrNjoOce5-m0ciJSHJtPX5oHwGHs,46987 +sqlalchemy/orm/descriptor_props.py,sha256=L1-Fpt0ss2zd9yV-HuVshnA343mB5Ous2sPlEVviCqM,25987 +sqlalchemy/orm/dynamic.py,sha256=lKXKi3VlFFLYvTLGxQBovSs_87wn1Nb5BwiG5AieMug,15819 +sqlalchemy/orm/evaluator.py,sha256=dwZ9jDx4Ooay0lBs2mL5RjLj2fisUaNrwFWkYJtsS1Y,6852 +sqlalchemy/orm/events.py,sha256=xKIVaKg14lk1o3s4NWtIuAUnQw3ZPHP92Ve5zb_zCZg,110251 +sqlalchemy/orm/exc.py,sha256=3HLZcpE8ESh37Mzx711_PMhgQLUPzy2bX1-RVA2o8xw,6532 +sqlalchemy/orm/identity.py,sha256=GOb7b8he7SslK6Qd3CjjIThUI2eT4IPU6FM3buVFC-M,7233 +sqlalchemy/orm/instrumentation.py,sha256=-BxrpgaW-pznyd-9bsRM8ns69fGaojdO5qAxnHHz5Pw,20349 +sqlalchemy/orm/interfaces.py,sha256=dqN7rQfF-0LRsx0y9r2ya6BXRjS4wtuh_DTRyDZT0qI,28791 +sqlalchemy/orm/loading.py,sha256=a9a9cLOUi2vFgW_Sg2yelTRlJw_4PbJ6iJp6iRFDoK0,49316 +sqlalchemy/orm/mapper.py,sha256=RDXIXT1g5blx4uXC5N9CxsaUi8paDp8mfcR8CLnLlO0,139136 +sqlalchemy/orm/path_registry.py,sha256=_dDIuYcBdQoGjY2turOru31SDY8tORNk-Qa3LwMG-zw,16411 +sqlalchemy/orm/persistence.py,sha256=6sLRZp5aTgSlSEKE_m1YRT3vuFtb8B9URjUIeMyo--A,80422 +sqlalchemy/orm/properties.py,sha256=EPhbfqvoqrN7X76guY3Wl5LO2yIl9FQBXiVrRgH5RX0,14912 +sqlalchemy/orm/query.py,sha256=kqz6_2371rQIoHBE3OiOdg6NggfccVLyt5jMSV9Cq7Q,124763 +sqlalchemy/orm/relationships.py,sha256=7RS2NC565MGVh3duADE45yAW_u3bwUi3X6mLGd4rMNg,143340 +sqlalchemy/orm/scoping.py,sha256=AKNozuMi8T-YB7DDNeatnVIVVhJ0FxSCbP1DBBALUh0,7220 +sqlalchemy/orm/session.py,sha256=Dn3sv9-uDTR9sPdF1t4i_uU5u5G6OoAFrG3MQH56gM8,158731 +sqlalchemy/orm/state.py,sha256=actQlG4fvVtEapQxIObZDw6T5wfPPCCnDxwIaW51OUs,33409 +sqlalchemy/orm/strategies.py,sha256=zPEzy7xptnOEvSIXpEKGeG8ltdoCtcoS-aXxted1u2k,107887 +sqlalchemy/orm/strategy_options.py,sha256=Y5yWvCpLILolcjvaYK8RzwsGeM2rCevCtiBipQRcHb0,66753 +sqlalchemy/orm/sync.py,sha256=tE2dS0i3vekS1TfB7R-_hhvekOi_esfcB-0bSwajjck,5824 +sqlalchemy/orm/unitofwork.py,sha256=nRJ7fWzpiedk5ObQz2v5gojLBzml9W5Al4qNB6-JWoI,27090 +sqlalchemy/orm/util.py,sha256=EZNS_6y0aV75RPvv3w04yUlfFS6DlLsM8-2pfTCAWLM,72410 +sqlalchemy/pool/__init__.py,sha256=cQIwYAY52VyVgAKdA159zhdlS38Dy6fFWT7l-KWjubk,1603 +sqlalchemy/pool/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/pool/__pycache__/base.cpython-38.pyc,, +sqlalchemy/pool/__pycache__/dbapi_proxy.cpython-38.pyc,, +sqlalchemy/pool/__pycache__/events.cpython-38.pyc,, +sqlalchemy/pool/__pycache__/impl.cpython-38.pyc,, +sqlalchemy/pool/base.py,sha256=vKBUGS59GwHjbQu-9ZFLzRbAqowTa-UgL9pjPKnUwYg,38552 +sqlalchemy/pool/dbapi_proxy.py,sha256=mPGtLr9czWrlVm2INYS1yMDr8bx-8rxY4KbAKmAasTk,4229 +sqlalchemy/pool/events.py,sha256=Z4LB7zGyEh-6L5VcWLdSdZ9lfOgDnn_G939Y_Fx_Wc8,10046 +sqlalchemy/pool/impl.py,sha256=VpW58L1fSxPtXWAzPvZ8qFhDRd_QiLdXN5GcjG68KP4,15783 +sqlalchemy/processors.py,sha256=ZnVfpn3-SQyqBa-3bmrjVPu3WyB7wsCovqRAeQdOP0M,5745 +sqlalchemy/schema.py,sha256=SbqBYd5vstujoWgpBXav9hBz7uUernJlhDTMFE05b4s,2413 +sqlalchemy/sql/__init__.py,sha256=E8Itj6nV7prc9wxhZiLBNghWLgG-MplZv_K3kxPltfc,4661 +sqlalchemy/sql/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/annotation.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/base.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/coercions.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/compiler.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/crud.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/ddl.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/default_comparator.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/dml.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/elements.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/events.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/expression.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/functions.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/lambdas.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/naming.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/operators.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/roles.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/schema.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/selectable.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/sqltypes.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/traversals.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/type_api.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/util.cpython-38.pyc,, +sqlalchemy/sql/__pycache__/visitors.cpython-38.pyc,, +sqlalchemy/sql/annotation.py,sha256=90IWQimMll92PwzUzP1I0zX_DQBSreciHZspCIj1Hro,11235 +sqlalchemy/sql/base.py,sha256=QSJLUi-78qBt8DCt96aQ1BeNx8RgkC18lJ2fW6phWtc,55804 +sqlalchemy/sql/coercions.py,sha256=YG6Qxn3jdOGaTXxU9KYAMsXewiFt6hDqlaI4vQVH184,33982 +sqlalchemy/sql/compiler.py,sha256=gJW34BvzWRiMj-21RVuy9mg8H2XjL6dui0NcoLHnIh0,185150 +sqlalchemy/sql/crud.py,sha256=LvT3ktKZ1eRvA85k_xXdP2ReU8-EWRYFMJhk-B0uRmc,35420 +sqlalchemy/sql/ddl.py,sha256=mHHvmrcCuLhU3B5FitlOzN_2OTer4NX2lIUUPwRlp7U,44002 +sqlalchemy/sql/default_comparator.py,sha256=cWeQCeJExjrR1WE_kLSbOD2wXRPpNzWQ7e7WLCtqknc,11046 +sqlalchemy/sql/dml.py,sha256=IJtieRwwROzAOCGcxMsIR2BCo-7H5mslZNMBzYRs138,51654 +sqlalchemy/sql/elements.py,sha256=5UuBCaLQnhZJauf-oc86bMtNnqscZZg5DCHJs1QhTbg,179644 +sqlalchemy/sql/events.py,sha256=Fjlr6Bl-dWFyUJfJaSB5fzhrkGil4oNuVRiCGax05nA,13369 +sqlalchemy/sql/expression.py,sha256=U4nrgSoREcBFP9gjaT2SZByHsls4JS1TIBpobKv1Y7c,8828 +sqlalchemy/sql/functions.py,sha256=bZVBP3oNRQUHZOqVtXHeEywJZKWWfVoGLdg-Kdwau1Q,47344 +sqlalchemy/sql/lambdas.py,sha256=vl2xjN6EgnzIPVDDqDhMw7M7LMK1QvMfJ3Bvw2XDl7c,44238 +sqlalchemy/sql/naming.py,sha256=G1eXvRjbZ8QENRaOhSIj_8ZFAiqeqe4hHPpBKktXmns,6786 +sqlalchemy/sql/operators.py,sha256=HgEviZ28iLSRcsE343HUXhESUzVR_TaENpx3msuXbOM,47433 +sqlalchemy/sql/roles.py,sha256=bE-DYltzs2IGuzgY-4pzHnFdwH0EFqc4o88j178IF8E,5526 +sqlalchemy/sql/schema.py,sha256=cBzp3Zx40eGmGPoof98Uz5Fzqv80FTux3iJVOSeIyoo,188537 +sqlalchemy/sql/selectable.py,sha256=19rB7zrUqpjk3MFV2barBCrGoCP82Mn5h8L67RrcRiY,230760 +sqlalchemy/sql/sqltypes.py,sha256=tioX3SDE_uu-cCLbDjIC5-miUrlJkZqslcttEUiVoII,111862 +sqlalchemy/sql/traversals.py,sha256=vgbnT_A8cSgf5a23IXn8Qj3HkJJ2rpvIhyWLqLKeDco,49443 +sqlalchemy/sql/type_api.py,sha256=7QCCVwZ8QPzO_r2Vj-t7C47KVw3Utw3-AOOo6vHhMY0,64262 +sqlalchemy/sql/util.py,sha256=gS1-RY_-tU-UuZneIahlOUFV2irE6m6OX9eDSMMC6b4,35856 +sqlalchemy/sql/visitors.py,sha256=uPsWctSGvEC77lCGO2SgrIs6GONnIT0kkU6--SMrHvc,27316 +sqlalchemy/testing/__init__.py,sha256=2r5jKsKug5mSBWqc8szFQZjT-SEQ0S00ZUkDhvMaGaE,2768 +sqlalchemy/testing/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/assertions.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/assertsql.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/asyncio.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/config.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/engines.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/entities.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/exclusions.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/fixtures.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/mock.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/pickleable.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/profiling.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/provision.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/requirements.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/schema.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/util.cpython-38.pyc,, +sqlalchemy/testing/__pycache__/warnings.cpython-38.pyc,, +sqlalchemy/testing/assertions.py,sha256=vLuuP0Re7mg8aozEtk9GkKyN2tAcsYN7HZ-8rvCeDj8,24469 +sqlalchemy/testing/assertsql.py,sha256=VDeFE6B6MUOsW46Wjpef48ypTfbkwx1glm6ExuiZ28g,14964 +sqlalchemy/testing/asyncio.py,sha256=ffDzERQV3g2cpylQHdfuc1ZveK7v_Q8240cCdsaEFAk,3672 +sqlalchemy/testing/config.py,sha256=BokuYTNp-Nkcjb-x_IaF-FU869ONJE3k_wv52n7ojZ4,6543 +sqlalchemy/testing/engines.py,sha256=XOHhutDz2RHqfuVtzQxNlgrY4T8n5QqlwK4YOcdvZZs,12661 +sqlalchemy/testing/entities.py,sha256=lxagTVr0pqQb14fr3-pdpbHXSxlbYh5UK-jLazQcd3Q,3253 +sqlalchemy/testing/exclusions.py,sha256=i-QZY81gdxRQZ-TF5I_I2Q6P4iSJqPCIdCMpNVwAvTE,13329 +sqlalchemy/testing/fixtures.py,sha256=Y9JYGCnkgU2jXRQAYE3fj9uq5fGl0pRE3sjkkTSbsrc,25641 +sqlalchemy/testing/mock.py,sha256=bw0Ds9eMMBHEDzT6shKJxi-9fFMH6qB9D00QxedH4OY,894 +sqlalchemy/testing/pickleable.py,sha256=0Rfbbtj7LJIsYOKo_cbByUC4FnXYXLiXwHl1VwrtcW8,2707 +sqlalchemy/testing/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sqlalchemy/testing/plugin/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/testing/plugin/__pycache__/bootstrap.cpython-38.pyc,, +sqlalchemy/testing/plugin/__pycache__/plugin_base.cpython-38.pyc,, +sqlalchemy/testing/plugin/__pycache__/pytestplugin.cpython-38.pyc,, +sqlalchemy/testing/plugin/__pycache__/reinvent_fixtures_py2k.cpython-38.pyc,, +sqlalchemy/testing/plugin/bootstrap.py,sha256=0YdnGTKGRs2OKCD3dUD4ShTZ2MR8ikUHdrn4OSSROjI,1686 +sqlalchemy/testing/plugin/plugin_base.py,sha256=u9dgPRH9Q2y46j3MPYqePCnClVp-9ejUc2vg1s1016k,21545 +sqlalchemy/testing/plugin/pytestplugin.py,sha256=LivS94F7FeYzDP-O-lHAm-fqsVnMRIbXQTfByw1E6NA,25560 +sqlalchemy/testing/plugin/reinvent_fixtures_py2k.py,sha256=MdakbJzFh8N_7gUpX-nFbGPFs3AZRsmDAe-7zucf0ls,3288 +sqlalchemy/testing/profiling.py,sha256=q_4rhjMpb0nWBZ7K_JkuQMLKPcI-1kiB27_EKI49CDw,10566 +sqlalchemy/testing/provision.py,sha256=YUEX9eiHBnQYpTHKBWM9IBMoVRFIgm6sjcZIqOeyKIc,12047 +sqlalchemy/testing/requirements.py,sha256=A5cZGfEHGE1BcvSkciHmTy7_UyrdZjT7NNHK5JK0_yw,42342 +sqlalchemy/testing/schema.py,sha256=0IHnIuEHYMqjdSIjMkn7dUKSZoWbY7ou4SWGQY5X13o,6544 +sqlalchemy/testing/suite/__init__.py,sha256=_firVc2uS3TMZ3vH2baQzNb17ubM78RHtb9kniSybmk,476 +sqlalchemy/testing/suite/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/testing/suite/__pycache__/test_cte.cpython-38.pyc,, +sqlalchemy/testing/suite/__pycache__/test_ddl.cpython-38.pyc,, +sqlalchemy/testing/suite/__pycache__/test_deprecations.cpython-38.pyc,, +sqlalchemy/testing/suite/__pycache__/test_dialect.cpython-38.pyc,, +sqlalchemy/testing/suite/__pycache__/test_insert.cpython-38.pyc,, +sqlalchemy/testing/suite/__pycache__/test_reflection.cpython-38.pyc,, +sqlalchemy/testing/suite/__pycache__/test_results.cpython-38.pyc,, +sqlalchemy/testing/suite/__pycache__/test_rowcount.cpython-38.pyc,, +sqlalchemy/testing/suite/__pycache__/test_select.cpython-38.pyc,, +sqlalchemy/testing/suite/__pycache__/test_sequence.cpython-38.pyc,, +sqlalchemy/testing/suite/__pycache__/test_types.cpython-38.pyc,, +sqlalchemy/testing/suite/__pycache__/test_unicode_ddl.cpython-38.pyc,, +sqlalchemy/testing/suite/__pycache__/test_update_delete.cpython-38.pyc,, +sqlalchemy/testing/suite/test_cte.py,sha256=XuTuaWblSXyO1OOUTShBBmNch7fBdGnlMD84ooVTqFY,6183 +sqlalchemy/testing/suite/test_ddl.py,sha256=UwbfljXHdWUen3muIcgnOPi-A4AO6F1QzSOiHf9lU-A,11762 +sqlalchemy/testing/suite/test_deprecations.py,sha256=8oLDFUswey8KjPFKRUsqMyGT5sUMMoPQr7-XyIBMehw,5059 +sqlalchemy/testing/suite/test_dialect.py,sha256=eR1VVOb2fm955zavpWkmMjipCva3QvEE177U0OG-0LY,10895 +sqlalchemy/testing/suite/test_insert.py,sha256=oKtVjFuxqdSV5uKj5-OxdSABupLp0pECkWkSLd2U_QA,11134 +sqlalchemy/testing/suite/test_reflection.py,sha256=BJkKloxhujHSTNuwL29EyiSTd2A77ZJhTtc0GrBcnd0,57615 +sqlalchemy/testing/suite/test_results.py,sha256=xcoSl1ueaHo8LgKZp0Z1lJ44Mhjf2hxlWs_LjNLBNiE,13983 +sqlalchemy/testing/suite/test_rowcount.py,sha256=GQQRXIWbb6SfD5hwtBC8qvkGAgi1rI5Pv3c59eoumck,4877 +sqlalchemy/testing/suite/test_select.py,sha256=is3BbULeOWOJTRCoUwPnh6Crue15FXfkXKqAkxrFeGM,55464 +sqlalchemy/testing/suite/test_sequence.py,sha256=eCyOQlynF8T0cLrIMz0PO6WuW8ktpFVYq_fQp5CQ298,8431 +sqlalchemy/testing/suite/test_types.py,sha256=ZOWB06wLYK8sQc6RyZfB8y3GOViVlO6xF9UjmKS5F5A,45839 +sqlalchemy/testing/suite/test_unicode_ddl.py,sha256=CndeAtV3DWJXxLbOoumqf4_mOOYcW_yNOrbKQ4cwFhw,6737 +sqlalchemy/testing/suite/test_update_delete.py,sha256=ebU5oV9hUZCW1ZBaZ-YAnxQE2Nk6GQashkOy6FOsp_c,1587 +sqlalchemy/testing/util.py,sha256=ZtMew3LnhnKuL8V7oeQ9YC5rv4ZExSKdKh5VxVyjDj0,12503 +sqlalchemy/testing/warnings.py,sha256=BIE5FlG03PHlZ4oy-kx5IIO78q0m_rahAvW8pDJhoxc,2347 +sqlalchemy/types.py,sha256=u2qTsqsdmtsedDKSDS9B1JDEvKGtvmWgHLjrj-YvLyk,2936 +sqlalchemy/util/__init__.py,sha256=F39iHcW8H3sxjhJcZ2_Y0SRoKrLtIZzEB4Rwj6B79dQ,6347 +sqlalchemy/util/__pycache__/__init__.cpython-38.pyc,, +sqlalchemy/util/__pycache__/_collections.cpython-38.pyc,, +sqlalchemy/util/__pycache__/_compat_py3k.cpython-38.pyc,, +sqlalchemy/util/__pycache__/_concurrency_py3k.cpython-38.pyc,, +sqlalchemy/util/__pycache__/_preloaded.cpython-38.pyc,, +sqlalchemy/util/__pycache__/compat.cpython-38.pyc,, +sqlalchemy/util/__pycache__/concurrency.cpython-38.pyc,, +sqlalchemy/util/__pycache__/deprecations.cpython-38.pyc,, +sqlalchemy/util/__pycache__/langhelpers.cpython-38.pyc,, +sqlalchemy/util/__pycache__/queue.cpython-38.pyc,, +sqlalchemy/util/__pycache__/topological.cpython-38.pyc,, +sqlalchemy/util/_collections.py,sha256=BhJPIHmzZ56K35OdqUhxueitkG-_DXqq2VfNggPzD4U,29139 +sqlalchemy/util/_compat_py3k.py,sha256=VSDmbXqhWhW_z4ysSdIYbGW_nbhAJKbEEFUINLEGobI,2195 +sqlalchemy/util/_concurrency_py3k.py,sha256=SOgSeqpdQ6SM_o5HSBMRRWX5Att8c4w05oK18xy0nKY,6780 +sqlalchemy/util/_preloaded.py,sha256=SGizwMVpZcVk_4OFVBkYuB1ISaySciSstyel8OAptIk,2396 +sqlalchemy/util/compat.py,sha256=tk5GQ_Wh6DhUJ35FpbUfH9f-pVMMFi54Q8URlpYEOsI,18245 +sqlalchemy/util/concurrency.py,sha256=j5c0L2qNWjBzcORYZ5dcA16qPrw3RXnhbqBf7PppegI,2117 +sqlalchemy/util/deprecations.py,sha256=ycHV6JnmF1yoV_Ww_4kQ3L_RCksc7BXHLYHWuBArNHw,11729 +sqlalchemy/util/langhelpers.py,sha256=FxMJByLfGzagAkHqSGofTCN8GVSOmahtkuH1z_16Trw,56250 +sqlalchemy/util/queue.py,sha256=WvS8AimNmR8baB-QDbHJe9F4RT9e05bYLxiVPouzNLk,9293 +sqlalchemy/util/topological.py,sha256=FtPkCjm8J6RU3sHZqM5AmQZCsqHfGfugu41pU8GS35k,2859 diff --git a/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/WHEEL b/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/WHEEL new file mode 100644 index 000000000..d5e85bd31 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: false +Tag: cp38-cp38-macosx_10_14_x86_64 + diff --git a/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/top_level.txt b/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/top_level.txt new file mode 100644 index 000000000..39fb2befb --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/SQLAlchemy-1.4.27.dist-info/top_level.txt @@ -0,0 +1 @@ +sqlalchemy diff --git a/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/INSTALLER b/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/LICENSE.rst b/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/LICENSE.rst new file mode 100644 index 000000000..c37cae49e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/LICENSE.rst @@ -0,0 +1,28 @@ +Copyright 2007 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/METADATA b/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/METADATA new file mode 100644 index 000000000..b58b9bd5f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/METADATA @@ -0,0 +1,129 @@ +Metadata-Version: 2.1 +Name: Werkzeug +Version: 2.0.2 +Summary: The comprehensive WSGI web application library. +Home-page: https://palletsprojects.com/p/werkzeug/ +Author: Armin Ronacher +Author-email: armin.ronacher@active-4.com +Maintainer: Pallets +Maintainer-email: contact@palletsprojects.com +License: BSD-3-Clause +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Documentation, https://werkzeug.palletsprojects.com/ +Project-URL: Changes, https://werkzeug.palletsprojects.com/changes/ +Project-URL: Source Code, https://github.com/pallets/werkzeug/ +Project-URL: Issue Tracker, https://github.com/pallets/werkzeug/issues/ +Project-URL: Twitter, https://twitter.com/PalletsTeam +Project-URL: Chat, https://discord.gg/pallets +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware +Classifier: Topic :: Software Development :: Libraries :: Application Frameworks +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst +License-File: LICENSE.rst +Requires-Dist: dataclasses ; python_version < "3.7" +Provides-Extra: watchdog +Requires-Dist: watchdog ; extra == 'watchdog' + +Werkzeug +======== + +*werkzeug* German noun: "tool". Etymology: *werk* ("work"), *zeug* ("stuff") + +Werkzeug is a comprehensive `WSGI`_ web application library. It began as +a simple collection of various utilities for WSGI applications and has +become one of the most advanced WSGI utility libraries. + +It includes: + +- An interactive debugger that allows inspecting stack traces and + source code in the browser with an interactive interpreter for any + frame in the stack. +- A full-featured request object with objects to interact with + headers, query args, form data, files, and cookies. +- A response object that can wrap other WSGI applications and handle + streaming data. +- A routing system for matching URLs to endpoints and generating URLs + for endpoints, with an extensible system for capturing variables + from URLs. +- HTTP utilities to handle entity tags, cache control, dates, user + agents, cookies, files, and more. +- A threaded WSGI server for use while developing applications + locally. +- A test client for simulating HTTP requests during testing without + requiring running a server. + +Werkzeug doesn't enforce any dependencies. It is up to the developer to +choose a template engine, database adapter, and even how to handle +requests. It can be used to build all sorts of end user applications +such as blogs, wikis, or bulletin boards. + +`Flask`_ wraps Werkzeug, using it to handle the details of WSGI while +providing more structure and patterns for defining powerful +applications. + +.. _WSGI: https://wsgi.readthedocs.io/en/latest/ +.. _Flask: https://www.palletsprojects.com/p/flask/ + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + pip install -U Werkzeug + +.. _pip: https://pip.pypa.io/en/stable/getting-started/ + + +A Simple Example +---------------- + +.. code-block:: python + + from werkzeug.wrappers import Request, Response + + @Request.application + def application(request): + return Response('Hello, World!') + + if __name__ == '__main__': + from werkzeug.serving import run_simple + run_simple('localhost', 4000, application) + + +Donate +------ + +The Pallets organization develops and supports Werkzeug and other +popular packages. In order to grow the community of contributors and +users, and allow the maintainers to devote more time to the projects, +`please donate today`_. + +.. _please donate today: https://palletsprojects.com/donate + + +Links +----- + +- Documentation: https://werkzeug.palletsprojects.com/ +- Changes: https://werkzeug.palletsprojects.com/changes/ +- PyPI Releases: https://pypi.org/project/Werkzeug/ +- Source Code: https://github.com/pallets/werkzeug/ +- Issue Tracker: https://github.com/pallets/werkzeug/issues/ +- Website: https://palletsprojects.com/p/werkzeug/ +- Twitter: https://twitter.com/PalletsTeam +- Chat: https://discord.gg/pallets + + diff --git a/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/RECORD b/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/RECORD new file mode 100644 index 000000000..a566dd6fa --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/RECORD @@ -0,0 +1,111 @@ +Werkzeug-2.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +Werkzeug-2.0.2.dist-info/LICENSE.rst,sha256=O0nc7kEF6ze6wQ-vG-JgQI_oXSUrjp3y4JefweCUQ3s,1475 +Werkzeug-2.0.2.dist-info/METADATA,sha256=vh_xrARtpmkFYnWRAgfSiHgl66LH143rMfAfPZo-R_E,4452 +Werkzeug-2.0.2.dist-info/RECORD,, +Werkzeug-2.0.2.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +Werkzeug-2.0.2.dist-info/top_level.txt,sha256=QRyj2VjwJoQkrwjwFIOlB8Xg3r9un0NtqVHQF-15xaw,9 +werkzeug/__init__.py,sha256=Wx1PLCftJ7UAS0fBXEO4Prdr6kvEQ124Stwg-XwyhW4,188 +werkzeug/__pycache__/__init__.cpython-38.pyc,, +werkzeug/__pycache__/_internal.cpython-38.pyc,, +werkzeug/__pycache__/_reloader.cpython-38.pyc,, +werkzeug/__pycache__/datastructures.cpython-38.pyc,, +werkzeug/__pycache__/exceptions.cpython-38.pyc,, +werkzeug/__pycache__/filesystem.cpython-38.pyc,, +werkzeug/__pycache__/formparser.cpython-38.pyc,, +werkzeug/__pycache__/http.cpython-38.pyc,, +werkzeug/__pycache__/local.cpython-38.pyc,, +werkzeug/__pycache__/routing.cpython-38.pyc,, +werkzeug/__pycache__/security.cpython-38.pyc,, +werkzeug/__pycache__/serving.cpython-38.pyc,, +werkzeug/__pycache__/test.cpython-38.pyc,, +werkzeug/__pycache__/testapp.cpython-38.pyc,, +werkzeug/__pycache__/urls.cpython-38.pyc,, +werkzeug/__pycache__/user_agent.cpython-38.pyc,, +werkzeug/__pycache__/useragents.cpython-38.pyc,, +werkzeug/__pycache__/utils.cpython-38.pyc,, +werkzeug/__pycache__/wsgi.cpython-38.pyc,, +werkzeug/_internal.py,sha256=_QKkvdaG4pDFwK68c0EpPzYJGe9Y7toRAT1cBbC-CxU,18572 +werkzeug/_reloader.py,sha256=B1hEfgsUOz2IginBQM5Zak_eaIF7gr3GS5-0x2OHvAE,13950 +werkzeug/datastructures.py,sha256=m79A8rHQEt5B7qVqyrjARXzHL66Katn8S92urGscTw4,97929 +werkzeug/datastructures.pyi,sha256=CoVwrQ2Vr9JnbprNL9aE3vOz8mOejT9qysQ-BT53C8Y,34089 +werkzeug/debug/__init__.py,sha256=jYA1e1Gw_8EPOytr-BoMdmm0rzP-Z1H0Ih7wIObnKwQ,17968 +werkzeug/debug/__pycache__/__init__.cpython-38.pyc,, +werkzeug/debug/__pycache__/console.cpython-38.pyc,, +werkzeug/debug/__pycache__/repr.cpython-38.pyc,, +werkzeug/debug/__pycache__/tbtools.cpython-38.pyc,, +werkzeug/debug/console.py,sha256=E1nBMEvFkX673ShQjPtVY-byYatfX9MN-dBMjRI8a8E,5897 +werkzeug/debug/repr.py,sha256=QCSHENKsChEZDCIApkVi_UNjhJ77v8BMXK1OfxO189M,9483 +werkzeug/debug/shared/FONT_LICENSE,sha256=LwAVEI1oYnvXiNMT9SnCH_TaLCxCpeHziDrMg0gPkAI,4673 +werkzeug/debug/shared/ICON_LICENSE.md,sha256=DhA6Y1gUl5Jwfg0NFN9Rj4VWITt8tUx0IvdGf0ux9-s,222 +werkzeug/debug/shared/console.png,sha256=bxax6RXXlvOij_KeqvSNX0ojJf83YbnZ7my-3Gx9w2A,507 +werkzeug/debug/shared/debugger.js,sha256=tg42SZs1SVmYWZ-_Fj5ELK5-FLHnGNQrei0K2By8Bw8,10521 +werkzeug/debug/shared/less.png,sha256=-4-kNRaXJSONVLahrQKUxMwXGm9R4OnZ9SxDGpHlIR4,191 +werkzeug/debug/shared/more.png,sha256=GngN7CioHQoV58rH6ojnkYi8c_qED2Aka5FO5UXrReY,200 +werkzeug/debug/shared/source.png,sha256=RoGcBTE4CyCB85GBuDGTFlAnUqxwTBiIfDqW15EpnUQ,818 +werkzeug/debug/shared/style.css,sha256=h1ZSUVaKNpfbfcYzRb513WAhPySGDQom1uih3uEDxPw,6704 +werkzeug/debug/shared/ubuntu.ttf,sha256=1eaHFyepmy4FyDvjLVzpITrGEBu_CZYY94jE0nED1c0,70220 +werkzeug/debug/tbtools.py,sha256=AFRrjLDCAps7G5K2-RxNZpXXaEoeFHm68T00f4vlDYA,19362 +werkzeug/exceptions.py,sha256=CUwx0pBiNbk4f9cON17ekgKnmLi6HIVFjUmYZc2x0wM,28681 +werkzeug/filesystem.py,sha256=JS2Dv2QF98WILxY4_thHl-WMcUcwluF_4igkDPaP1l4,1956 +werkzeug/formparser.py,sha256=X-p3Ek4ji8XrKrbmaWxr8StLSc6iuksbpIeweaabs4s,17400 +werkzeug/http.py,sha256=oUCXFFMnkOQ-cHbUY_aiqitshcrSzNDq3fEMf1VI_yk,45141 +werkzeug/local.py,sha256=bwL-y3-qOZAspJ66W1P36SUApLXJy3UY8nLYbM9kfmY,23183 +werkzeug/middleware/__init__.py,sha256=qfqgdT5npwG9ses3-FXQJf3aB95JYP1zchetH_T3PUw,500 +werkzeug/middleware/__pycache__/__init__.cpython-38.pyc,, +werkzeug/middleware/__pycache__/dispatcher.cpython-38.pyc,, +werkzeug/middleware/__pycache__/http_proxy.cpython-38.pyc,, +werkzeug/middleware/__pycache__/lint.cpython-38.pyc,, +werkzeug/middleware/__pycache__/profiler.cpython-38.pyc,, +werkzeug/middleware/__pycache__/proxy_fix.cpython-38.pyc,, +werkzeug/middleware/__pycache__/shared_data.cpython-38.pyc,, +werkzeug/middleware/dispatcher.py,sha256=Fh_w-KyWnTSYF-Lfv5dimQ7THSS7afPAZMmvc4zF1gg,2580 +werkzeug/middleware/http_proxy.py,sha256=HE8VyhS7CR-E1O6_9b68huv8FLgGGR1DLYqkS3Xcp3Q,7558 +werkzeug/middleware/lint.py,sha256=sAg3GcOhICIkwYX5bJGG8n8iebX0Yipq_UH0HvrBvoU,13964 +werkzeug/middleware/profiler.py,sha256=QkXk7cqnaPnF8wQu-5SyPCIOT3_kdABUBorQOghVNOA,4899 +werkzeug/middleware/proxy_fix.py,sha256=uRgQ3dEvFV8JxUqajHYYYOPEeA_BFqaa51Yp8VW0uzA,6849 +werkzeug/middleware/shared_data.py,sha256=xydEqOhAGg0aQJEllPDVfz2-8jHwWvJpAxfPsfPCu7k,10960 +werkzeug/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +werkzeug/routing.py,sha256=oqJ32sWIZtIF6zbqfrnwB1Pbv2ShNwPDJd6FYqxdYVo,84527 +werkzeug/sansio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +werkzeug/sansio/__pycache__/__init__.cpython-38.pyc,, +werkzeug/sansio/__pycache__/multipart.cpython-38.pyc,, +werkzeug/sansio/__pycache__/request.cpython-38.pyc,, +werkzeug/sansio/__pycache__/response.cpython-38.pyc,, +werkzeug/sansio/__pycache__/utils.cpython-38.pyc,, +werkzeug/sansio/multipart.py,sha256=bJMCNC2f5xyAaylahNViJ0JqmV4ThLRbDVGVzKwcqrQ,8751 +werkzeug/sansio/request.py,sha256=aA9rABkWiG4MhYMByanst2NXkEclsq8SIxhb0LQf0e0,20228 +werkzeug/sansio/response.py,sha256=zvCq9HSBBZGBd5Gg412BY9RZIwnKsJl5Kzfd3Kl9sSo,26098 +werkzeug/sansio/utils.py,sha256=V5v-UUnX8pm4RehP9Tt_NiUSOJGJGUvKjlW0eOIQldM,4164 +werkzeug/security.py,sha256=gPDRuCjkjWrcqj99tBMq8_nHFZLFQjgoW5Ga5XIw9jo,8158 +werkzeug/serving.py,sha256=AfgLn0yKr9qXknmwO-0KXJ055oloS4h5DIFDHEu8iHA,38088 +werkzeug/test.py,sha256=8gE1l-Y9yAh2i3SI0kgpxIaI4oYZuehIkxxyDFcz6J0,48123 +werkzeug/testapp.py,sha256=f48prWSGJhbSrvYb8e1fnAah4BkrLb0enHSdChgsjBY,9471 +werkzeug/urls.py,sha256=Du2lreBHvgBh5c2_bcx72g3hzV2ZabXYZsp-picUIJs,41023 +werkzeug/user_agent.py,sha256=WclZhpvgLurMF45hsioSbS75H1Zb4iMQGKN3_yZ2oKo,1420 +werkzeug/useragents.py,sha256=G8tmv_6vxJaPrLQH3eODNgIYe0_V6KETROQlJI-WxDE,7264 +werkzeug/utils.py,sha256=D_dnCLUfodQ4k0GRSpnI6qDoVoaX7-Dza57bx7sabG0,37101 +werkzeug/wrappers/__init__.py,sha256=-s75nPbyXHzU_rwmLPDhoMuGbEUk0jZT_n0ZQAOFGf8,654 +werkzeug/wrappers/__pycache__/__init__.cpython-38.pyc,, +werkzeug/wrappers/__pycache__/accept.cpython-38.pyc,, +werkzeug/wrappers/__pycache__/auth.cpython-38.pyc,, +werkzeug/wrappers/__pycache__/base_request.cpython-38.pyc,, +werkzeug/wrappers/__pycache__/base_response.cpython-38.pyc,, +werkzeug/wrappers/__pycache__/common_descriptors.cpython-38.pyc,, +werkzeug/wrappers/__pycache__/cors.cpython-38.pyc,, +werkzeug/wrappers/__pycache__/etag.cpython-38.pyc,, +werkzeug/wrappers/__pycache__/json.cpython-38.pyc,, +werkzeug/wrappers/__pycache__/request.cpython-38.pyc,, +werkzeug/wrappers/__pycache__/response.cpython-38.pyc,, +werkzeug/wrappers/__pycache__/user_agent.cpython-38.pyc,, +werkzeug/wrappers/accept.py,sha256=_oZtAQkahvsrPRkNj2fieg7_St9P0NFC3SgZbJKS6xU,429 +werkzeug/wrappers/auth.py,sha256=rZPCzGxHk9R55PRkmS90kRywUVjjuMWzCGtH68qCq8U,856 +werkzeug/wrappers/base_request.py,sha256=saz9RyNQkvI_XLPYVm29KijNHmD1YzgxDqa0qHTbgss,1174 +werkzeug/wrappers/base_response.py,sha256=q_-TaYywT5G4zA-DWDRDJhJSat2_4O7gOPob6ye4_9A,1186 +werkzeug/wrappers/common_descriptors.py,sha256=v_kWLH3mvCiSRVJ1FNw7nO3w2UJfzY57UKKB5J4zCvE,898 +werkzeug/wrappers/cors.py,sha256=c5UndlZsZvYkbPrp6Gj5iSXxw_VOJDJHskO6-jRmNyQ,846 +werkzeug/wrappers/etag.py,sha256=XHWQQs7Mdd1oWezgBIsl-bYe8ydKkRZVil2Qd01D0Mo,846 +werkzeug/wrappers/json.py,sha256=HM1btPseGeXca0vnwQN_MvZl6h-qNsFY5YBKXKXFwus,410 +werkzeug/wrappers/request.py,sha256=yZGplfC3UqNuykwLJmgywiMhmnoKEGHJOZn_A_ublcQ,24822 +werkzeug/wrappers/response.py,sha256=0n8OcQptiM2e550SALLeg7vC1uWsUbCeE1rPZFfXR78,35177 +werkzeug/wrappers/user_agent.py,sha256=Wl1-A0-1r8o7cHIZQTB55O4Ged6LpCKENaQDlOY5pXA,435 +werkzeug/wsgi.py,sha256=L7s5-Rlt7BRVEZ1m81MaenGfMDP7yL3p1Kxt9Yssqzg,33727 diff --git a/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/WHEEL b/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/WHEEL new file mode 100644 index 000000000..5bad85fdc --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/top_level.txt b/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/top_level.txt new file mode 100644 index 000000000..6fe8da849 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/Werkzeug-2.0.2.dist-info/top_level.txt @@ -0,0 +1 @@ +werkzeug diff --git a/flask-app/venv/lib/python3.8/site-packages/__pycache__/ddtrace_gevent_check.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/__pycache__/ddtrace_gevent_check.cpython-38.pyc new file mode 100644 index 000000000..9e6a024c6 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/__pycache__/ddtrace_gevent_check.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/__pycache__/easy_install.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/__pycache__/easy_install.cpython-38.pyc new file mode 100644 index 000000000..916a571a7 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/__pycache__/easy_install.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/__pycache__/pyparsing.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/__pycache__/pyparsing.cpython-38.pyc new file mode 100644 index 000000000..5d5883e3e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/__pycache__/pyparsing.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/__pycache__/six.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/__pycache__/six.cpython-38.pyc new file mode 100644 index 000000000..97e2c3b72 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/__pycache__/six.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/__init__.py b/flask-app/venv/lib/python3.8/site-packages/attr/__init__.py new file mode 100644 index 000000000..b1ce7fe24 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/__init__.py @@ -0,0 +1,78 @@ +from __future__ import absolute_import, division, print_function + +import sys + +from functools import partial + +from . import converters, exceptions, filters, setters, validators +from ._cmp import cmp_using +from ._config import get_run_validators, set_run_validators +from ._funcs import asdict, assoc, astuple, evolve, has, resolve_types +from ._make import ( + NOTHING, + Attribute, + Factory, + attrib, + attrs, + fields, + fields_dict, + make_class, + validate, +) +from ._version_info import VersionInfo + + +__version__ = "21.2.0" +__version_info__ = VersionInfo._from_version_string(__version__) + +__title__ = "attrs" +__description__ = "Classes Without Boilerplate" +__url__ = "https://www.attrs.org/" +__uri__ = __url__ +__doc__ = __description__ + " <" + __uri__ + ">" + +__author__ = "Hynek Schlawack" +__email__ = "hs@ox.cx" + +__license__ = "MIT" +__copyright__ = "Copyright (c) 2015 Hynek Schlawack" + + +s = attributes = attrs +ib = attr = attrib +dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) + +__all__ = [ + "Attribute", + "Factory", + "NOTHING", + "asdict", + "assoc", + "astuple", + "attr", + "attrib", + "attributes", + "attrs", + "cmp_using", + "converters", + "evolve", + "exceptions", + "fields", + "fields_dict", + "filters", + "get_run_validators", + "has", + "ib", + "make_class", + "resolve_types", + "s", + "set_run_validators", + "setters", + "validate", + "validators", +] + +if sys.version_info[:2] >= (3, 6): + from ._next_gen import define, field, frozen, mutable + + __all__.extend((define, field, frozen, mutable)) diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/__init__.pyi b/flask-app/venv/lib/python3.8/site-packages/attr/__init__.pyi new file mode 100644 index 000000000..3503b073b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/__init__.pyi @@ -0,0 +1,475 @@ +import sys + +from typing import ( + Any, + Callable, + Dict, + Generic, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, + overload, +) + +# `import X as X` is required to make these public +from . import converters as converters +from . import exceptions as exceptions +from . import filters as filters +from . import setters as setters +from . import validators as validators +from ._version_info import VersionInfo + + +__version__: str +__version_info__: VersionInfo +__title__: str +__description__: str +__url__: str +__uri__: str +__author__: str +__email__: str +__license__: str +__copyright__: str + +_T = TypeVar("_T") +_C = TypeVar("_C", bound=type) + +_EqOrderType = Union[bool, Callable[[Any], Any]] +_ValidatorType = Callable[[Any, Attribute[_T], _T], Any] +_ConverterType = Callable[[Any], Any] +_FilterType = Callable[[Attribute[_T], _T], bool] +_ReprType = Callable[[Any], str] +_ReprArgType = Union[bool, _ReprType] +_OnSetAttrType = Callable[[Any, Attribute[Any], Any], Any] +_OnSetAttrArgType = Union[ + _OnSetAttrType, List[_OnSetAttrType], setters._NoOpType +] +_FieldTransformer = Callable[[type, List[Attribute[Any]]], List[Attribute[Any]]] +# FIXME: in reality, if multiple validators are passed they must be in a list +# or tuple, but those are invariant and so would prevent subtypes of +# _ValidatorType from working when passed in a list or tuple. +_ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] + +# _make -- + +NOTHING: object + +# NOTE: Factory lies about its return type to make this possible: +# `x: List[int] # = Factory(list)` +# Work around mypy issue #4554 in the common case by using an overload. +if sys.version_info >= (3, 8): + from typing import Literal + + @overload + def Factory(factory: Callable[[], _T]) -> _T: ... + @overload + def Factory( + factory: Callable[[Any], _T], + takes_self: Literal[True], + ) -> _T: ... + @overload + def Factory( + factory: Callable[[], _T], + takes_self: Literal[False], + ) -> _T: ... +else: + @overload + def Factory(factory: Callable[[], _T]) -> _T: ... + @overload + def Factory( + factory: Union[Callable[[Any], _T], Callable[[], _T]], + takes_self: bool = ..., + ) -> _T: ... + +# Static type inference support via __dataclass_transform__ implemented as per: +# https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md +# This annotation must be applied to all overloads of "define" and "attrs" +# +# NOTE: This is a typing construct and does not exist at runtime. Extensions +# wrapping attrs decorators should declare a separate __dataclass_transform__ +# signature in the extension module using the specification linked above to +# provide pyright support. +def __dataclass_transform__( + *, + eq_default: bool = True, + order_default: bool = False, + kw_only_default: bool = False, + field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()), +) -> Callable[[_T], _T]: ... + +class Attribute(Generic[_T]): + name: str + default: Optional[_T] + validator: Optional[_ValidatorType[_T]] + repr: _ReprArgType + cmp: _EqOrderType + eq: _EqOrderType + order: _EqOrderType + hash: Optional[bool] + init: bool + converter: Optional[_ConverterType] + metadata: Dict[Any, Any] + type: Optional[Type[_T]] + kw_only: bool + on_setattr: _OnSetAttrType + + def evolve(self, **changes: Any) -> "Attribute[Any]": ... + +# NOTE: We had several choices for the annotation to use for type arg: +# 1) Type[_T] +# - Pros: Handles simple cases correctly +# - Cons: Might produce less informative errors in the case of conflicting +# TypeVars e.g. `attr.ib(default='bad', type=int)` +# 2) Callable[..., _T] +# - Pros: Better error messages than #1 for conflicting TypeVars +# - Cons: Terrible error messages for validator checks. +# e.g. attr.ib(type=int, validator=validate_str) +# -> error: Cannot infer function type argument +# 3) type (and do all of the work in the mypy plugin) +# - Pros: Simple here, and we could customize the plugin with our own errors. +# - Cons: Would need to write mypy plugin code to handle all the cases. +# We chose option #1. + +# `attr` lies about its return type to make the following possible: +# attr() -> Any +# attr(8) -> int +# attr(validator=) -> Whatever the callable expects. +# This makes this type of assignments possible: +# x: int = attr(8) +# +# This form catches explicit None or no default but with no other arguments +# returns Any. +@overload +def attrib( + default: None = ..., + validator: None = ..., + repr: _ReprArgType = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: None = ..., + converter: None = ..., + factory: None = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> Any: ... + +# This form catches an explicit None or no default and infers the type from the +# other arguments. +@overload +def attrib( + default: None = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> _T: ... + +# This form catches an explicit default argument. +@overload +def attrib( + default: _T, + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> _T: ... + +# This form covers type=non-Type: e.g. forward references (str), Any +@overload +def attrib( + default: Optional[_T] = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: object = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> Any: ... +@overload +def field( + *, + default: None = ..., + validator: None = ..., + repr: _ReprArgType = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + converter: None = ..., + factory: None = ..., + kw_only: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> Any: ... + +# This form catches an explicit None or no default and infers the type from the +# other arguments. +@overload +def field( + *, + default: None = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> _T: ... + +# This form catches an explicit default argument. +@overload +def field( + *, + default: _T, + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> _T: ... + +# This form covers type=non-Type: e.g. forward references (str), Any +@overload +def field( + *, + default: Optional[_T] = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: _ReprArgType = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + converter: Optional[_ConverterType] = ..., + factory: Optional[Callable[[], _T]] = ..., + kw_only: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., +) -> Any: ... +@overload +@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) +def attrs( + maybe_cls: _C, + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + auto_detect: bool = ..., + collect_by_mro: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., +) -> _C: ... +@overload +@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) +def attrs( + maybe_cls: None = ..., + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + auto_detect: bool = ..., + collect_by_mro: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., +) -> Callable[[_C], _C]: ... +@overload +@__dataclass_transform__(field_descriptors=(attrib, field)) +def define( + maybe_cls: _C, + *, + these: Optional[Dict[str, Any]] = ..., + repr: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + auto_detect: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., +) -> _C: ... +@overload +@__dataclass_transform__(field_descriptors=(attrib, field)) +def define( + maybe_cls: None = ..., + *, + these: Optional[Dict[str, Any]] = ..., + repr: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[bool] = ..., + order: Optional[bool] = ..., + auto_detect: bool = ..., + getstate_setstate: Optional[bool] = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., +) -> Callable[[_C], _C]: ... + +mutable = define +frozen = define # they differ only in their defaults + +# TODO: add support for returning NamedTuple from the mypy plugin +class _Fields(Tuple[Attribute[Any], ...]): + def __getattr__(self, name: str) -> Attribute[Any]: ... + +def fields(cls: type) -> _Fields: ... +def fields_dict(cls: type) -> Dict[str, Attribute[Any]]: ... +def validate(inst: Any) -> None: ... +def resolve_types( + cls: _C, + globalns: Optional[Dict[str, Any]] = ..., + localns: Optional[Dict[str, Any]] = ..., + attribs: Optional[List[Attribute[Any]]] = ..., +) -> _C: ... + +# TODO: add support for returning a proper attrs class from the mypy plugin +# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', +# [attr.ib()])` is valid +def make_class( + name: str, + attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]], + bases: Tuple[type, ...] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: Optional[_EqOrderType] = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: Optional[_EqOrderType] = ..., + order: Optional[_EqOrderType] = ..., + collect_by_mro: bool = ..., + on_setattr: Optional[_OnSetAttrArgType] = ..., + field_transformer: Optional[_FieldTransformer] = ..., +) -> type: ... + +# _funcs -- + +# TODO: add support for returning TypedDict from the mypy plugin +# FIXME: asdict/astuple do not honor their factory args. Waiting on one of +# these: +# https://github.com/python/mypy/issues/4236 +# https://github.com/python/typing/issues/253 +def asdict( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType[Any]] = ..., + dict_factory: Type[Mapping[Any, Any]] = ..., + retain_collection_types: bool = ..., + value_serializer: Optional[Callable[[type, Attribute[Any], Any], Any]] = ..., +) -> Dict[str, Any]: ... + +# TODO: add support for returning NamedTuple from the mypy plugin +def astuple( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType[Any]] = ..., + tuple_factory: Type[Sequence[Any]] = ..., + retain_collection_types: bool = ..., +) -> Tuple[Any, ...]: ... +def has(cls: type) -> bool: ... +def assoc(inst: _T, **changes: Any) -> _T: ... +def evolve(inst: _T, **changes: Any) -> _T: ... + +# _config -- + +def set_run_validators(run: bool) -> None: ... +def get_run_validators() -> bool: ... + +# aliases -- + +s = attributes = attrs +ib = attr = attrib +dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..9753ec103 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_cmp.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_cmp.cpython-38.pyc new file mode 100644 index 000000000..7c67c2962 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_cmp.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_compat.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_compat.cpython-38.pyc new file mode 100644 index 000000000..e2b4e7c5f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_compat.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_config.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_config.cpython-38.pyc new file mode 100644 index 000000000..9916d7354 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_config.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_funcs.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_funcs.cpython-38.pyc new file mode 100644 index 000000000..2519ab26c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_funcs.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_make.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_make.cpython-38.pyc new file mode 100644 index 000000000..0845cb40b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_make.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_next_gen.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_next_gen.cpython-38.pyc new file mode 100644 index 000000000..bea4c7105 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_next_gen.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_version_info.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_version_info.cpython-38.pyc new file mode 100644 index 000000000..11478bd2a Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/_version_info.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/converters.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/converters.cpython-38.pyc new file mode 100644 index 000000000..10e326091 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/converters.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/exceptions.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/exceptions.cpython-38.pyc new file mode 100644 index 000000000..203fc52ec Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/exceptions.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/filters.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/filters.cpython-38.pyc new file mode 100644 index 000000000..45430e03a Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/filters.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/setters.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/setters.cpython-38.pyc new file mode 100644 index 000000000..7aba53bb9 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/setters.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/validators.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/validators.cpython-38.pyc new file mode 100644 index 000000000..6448de5d0 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/attr/__pycache__/validators.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/_cmp.py b/flask-app/venv/lib/python3.8/site-packages/attr/_cmp.py new file mode 100644 index 000000000..b747b603f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/_cmp.py @@ -0,0 +1,152 @@ +from __future__ import absolute_import, division, print_function + +import functools + +from ._compat import new_class +from ._make import _make_ne + + +_operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="} + + +def cmp_using( + eq=None, + lt=None, + le=None, + gt=None, + ge=None, + require_same_type=True, + class_name="Comparable", +): + """ + Create a class that can be passed into `attr.ib`'s ``eq``, ``order``, and + ``cmp`` arguments to customize field comparison. + + The resulting class will have a full set of ordering methods if + at least one of ``{lt, le, gt, ge}`` and ``eq`` are provided. + + :param Optional[callable] eq: `callable` used to evaluate equality + of two objects. + :param Optional[callable] lt: `callable` used to evaluate whether + one object is less than another object. + :param Optional[callable] le: `callable` used to evaluate whether + one object is less than or equal to another object. + :param Optional[callable] gt: `callable` used to evaluate whether + one object is greater than another object. + :param Optional[callable] ge: `callable` used to evaluate whether + one object is greater than or equal to another object. + + :param bool require_same_type: When `True`, equality and ordering methods + will return `NotImplemented` if objects are not of the same type. + + :param Optional[str] class_name: Name of class. Defaults to 'Comparable'. + + See `comparison` for more details. + + .. versionadded:: 21.1.0 + """ + + body = { + "__slots__": ["value"], + "__init__": _make_init(), + "_requirements": [], + "_is_comparable_to": _is_comparable_to, + } + + # Add operations. + num_order_functions = 0 + has_eq_function = False + + if eq is not None: + has_eq_function = True + body["__eq__"] = _make_operator("eq", eq) + body["__ne__"] = _make_ne() + + if lt is not None: + num_order_functions += 1 + body["__lt__"] = _make_operator("lt", lt) + + if le is not None: + num_order_functions += 1 + body["__le__"] = _make_operator("le", le) + + if gt is not None: + num_order_functions += 1 + body["__gt__"] = _make_operator("gt", gt) + + if ge is not None: + num_order_functions += 1 + body["__ge__"] = _make_operator("ge", ge) + + type_ = new_class(class_name, (object,), {}, lambda ns: ns.update(body)) + + # Add same type requirement. + if require_same_type: + type_._requirements.append(_check_same_type) + + # Add total ordering if at least one operation was defined. + if 0 < num_order_functions < 4: + if not has_eq_function: + # functools.total_ordering requires __eq__ to be defined, + # so raise early error here to keep a nice stack. + raise ValueError( + "eq must be define is order to complete ordering from " + "lt, le, gt, ge." + ) + type_ = functools.total_ordering(type_) + + return type_ + + +def _make_init(): + """ + Create __init__ method. + """ + + def __init__(self, value): + """ + Initialize object with *value*. + """ + self.value = value + + return __init__ + + +def _make_operator(name, func): + """ + Create operator method. + """ + + def method(self, other): + if not self._is_comparable_to(other): + return NotImplemented + + result = func(self.value, other.value) + if result is NotImplemented: + return NotImplemented + + return result + + method.__name__ = "__%s__" % (name,) + method.__doc__ = "Return a %s b. Computed by attrs." % ( + _operation_names[name], + ) + + return method + + +def _is_comparable_to(self, other): + """ + Check whether `other` is comparable to `self`. + """ + for func in self._requirements: + if not func(self, other): + return False + return True + + +def _check_same_type(self, other): + """ + Return True if *self* and *other* are of the same type, False otherwise. + """ + return other.value.__class__ is self.value.__class__ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/_cmp.pyi b/flask-app/venv/lib/python3.8/site-packages/attr/_cmp.pyi new file mode 100644 index 000000000..7093550f0 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/_cmp.pyi @@ -0,0 +1,14 @@ +from typing import Type + +from . import _CompareWithType + + +def cmp_using( + eq: Optional[_CompareWithType], + lt: Optional[_CompareWithType], + le: Optional[_CompareWithType], + gt: Optional[_CompareWithType], + ge: Optional[_CompareWithType], + require_same_type: bool, + class_name: str, +) -> Type: ... diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/_compat.py b/flask-app/venv/lib/python3.8/site-packages/attr/_compat.py new file mode 100644 index 000000000..6939f338d --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/_compat.py @@ -0,0 +1,242 @@ +from __future__ import absolute_import, division, print_function + +import platform +import sys +import types +import warnings + + +PY2 = sys.version_info[0] == 2 +PYPY = platform.python_implementation() == "PyPy" + + +if PYPY or sys.version_info[:2] >= (3, 6): + ordered_dict = dict +else: + from collections import OrderedDict + + ordered_dict = OrderedDict + + +if PY2: + from collections import Mapping, Sequence + + from UserDict import IterableUserDict + + # We 'bundle' isclass instead of using inspect as importing inspect is + # fairly expensive (order of 10-15 ms for a modern machine in 2016) + def isclass(klass): + return isinstance(klass, (type, types.ClassType)) + + def new_class(name, bases, kwds, exec_body): + """ + A minimal stub of types.new_class that we need for make_class. + """ + ns = {} + exec_body(ns) + + return type(name, bases, ns) + + # TYPE is used in exceptions, repr(int) is different on Python 2 and 3. + TYPE = "type" + + def iteritems(d): + return d.iteritems() + + # Python 2 is bereft of a read-only dict proxy, so we make one! + class ReadOnlyDict(IterableUserDict): + """ + Best-effort read-only dict wrapper. + """ + + def __setitem__(self, key, val): + # We gently pretend we're a Python 3 mappingproxy. + raise TypeError( + "'mappingproxy' object does not support item assignment" + ) + + def update(self, _): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError( + "'mappingproxy' object has no attribute 'update'" + ) + + def __delitem__(self, _): + # We gently pretend we're a Python 3 mappingproxy. + raise TypeError( + "'mappingproxy' object does not support item deletion" + ) + + def clear(self): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError( + "'mappingproxy' object has no attribute 'clear'" + ) + + def pop(self, key, default=None): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError( + "'mappingproxy' object has no attribute 'pop'" + ) + + def popitem(self): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError( + "'mappingproxy' object has no attribute 'popitem'" + ) + + def setdefault(self, key, default=None): + # We gently pretend we're a Python 3 mappingproxy. + raise AttributeError( + "'mappingproxy' object has no attribute 'setdefault'" + ) + + def __repr__(self): + # Override to be identical to the Python 3 version. + return "mappingproxy(" + repr(self.data) + ")" + + def metadata_proxy(d): + res = ReadOnlyDict() + res.data.update(d) # We blocked update, so we have to do it like this. + return res + + def just_warn(*args, **kw): # pragma: no cover + """ + We only warn on Python 3 because we are not aware of any concrete + consequences of not setting the cell on Python 2. + """ + + +else: # Python 3 and later. + from collections.abc import Mapping, Sequence # noqa + + def just_warn(*args, **kw): + """ + We only warn on Python 3 because we are not aware of any concrete + consequences of not setting the cell on Python 2. + """ + warnings.warn( + "Running interpreter doesn't sufficiently support code object " + "introspection. Some features like bare super() or accessing " + "__class__ will not work with slotted classes.", + RuntimeWarning, + stacklevel=2, + ) + + def isclass(klass): + return isinstance(klass, type) + + TYPE = "class" + + def iteritems(d): + return d.items() + + new_class = types.new_class + + def metadata_proxy(d): + return types.MappingProxyType(dict(d)) + + +def make_set_closure_cell(): + """Return a function of two arguments (cell, value) which sets + the value stored in the closure cell `cell` to `value`. + """ + # pypy makes this easy. (It also supports the logic below, but + # why not do the easy/fast thing?) + if PYPY: + + def set_closure_cell(cell, value): + cell.__setstate__((value,)) + + return set_closure_cell + + # Otherwise gotta do it the hard way. + + # Create a function that will set its first cellvar to `value`. + def set_first_cellvar_to(value): + x = value + return + + # This function will be eliminated as dead code, but + # not before its reference to `x` forces `x` to be + # represented as a closure cell rather than a local. + def force_x_to_be_a_cell(): # pragma: no cover + return x + + try: + # Extract the code object and make sure our assumptions about + # the closure behavior are correct. + if PY2: + co = set_first_cellvar_to.func_code + else: + co = set_first_cellvar_to.__code__ + if co.co_cellvars != ("x",) or co.co_freevars != (): + raise AssertionError # pragma: no cover + + # Convert this code object to a code object that sets the + # function's first _freevar_ (not cellvar) to the argument. + if sys.version_info >= (3, 8): + # CPython 3.8+ has an incompatible CodeType signature + # (added a posonlyargcount argument) but also added + # CodeType.replace() to do this without counting parameters. + set_first_freevar_code = co.replace( + co_cellvars=co.co_freevars, co_freevars=co.co_cellvars + ) + else: + args = [co.co_argcount] + if not PY2: + args.append(co.co_kwonlyargcount) + args.extend( + [ + co.co_nlocals, + co.co_stacksize, + co.co_flags, + co.co_code, + co.co_consts, + co.co_names, + co.co_varnames, + co.co_filename, + co.co_name, + co.co_firstlineno, + co.co_lnotab, + # These two arguments are reversed: + co.co_cellvars, + co.co_freevars, + ] + ) + set_first_freevar_code = types.CodeType(*args) + + def set_closure_cell(cell, value): + # Create a function using the set_first_freevar_code, + # whose first closure cell is `cell`. Calling it will + # change the value of that cell. + setter = types.FunctionType( + set_first_freevar_code, {}, "setter", (), (cell,) + ) + # And call it to set the cell. + setter(value) + + # Make sure it works on this interpreter: + def make_func_with_cell(): + x = None + + def func(): + return x # pragma: no cover + + return func + + if PY2: + cell = make_func_with_cell().func_closure[0] + else: + cell = make_func_with_cell().__closure__[0] + set_closure_cell(cell, 100) + if cell.cell_contents != 100: + raise AssertionError # pragma: no cover + + except Exception: + return just_warn + else: + return set_closure_cell + + +set_closure_cell = make_set_closure_cell() diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/_config.py b/flask-app/venv/lib/python3.8/site-packages/attr/_config.py new file mode 100644 index 000000000..8ec920962 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/_config.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import, division, print_function + + +__all__ = ["set_run_validators", "get_run_validators"] + +_run_validators = True + + +def set_run_validators(run): + """ + Set whether or not validators are run. By default, they are run. + """ + if not isinstance(run, bool): + raise TypeError("'run' must be bool.") + global _run_validators + _run_validators = run + + +def get_run_validators(): + """ + Return whether or not validators are run. + """ + return _run_validators diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/_funcs.py b/flask-app/venv/lib/python3.8/site-packages/attr/_funcs.py new file mode 100644 index 000000000..fda508c5c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/_funcs.py @@ -0,0 +1,395 @@ +from __future__ import absolute_import, division, print_function + +import copy + +from ._compat import iteritems +from ._make import NOTHING, _obj_setattr, fields +from .exceptions import AttrsAttributeNotFoundError + + +def asdict( + inst, + recurse=True, + filter=None, + dict_factory=dict, + retain_collection_types=False, + value_serializer=None, +): + """ + Return the ``attrs`` attribute values of *inst* as a dict. + + Optionally recurse into other ``attrs``-decorated classes. + + :param inst: Instance of an ``attrs``-decorated class. + :param bool recurse: Recurse into classes that are also + ``attrs``-decorated. + :param callable filter: A callable whose return code determines whether an + attribute or element is included (``True``) or dropped (``False``). Is + called with the `attr.Attribute` as the first argument and the + value as the second argument. + :param callable dict_factory: A callable to produce dictionaries from. For + example, to produce ordered dictionaries instead of normal Python + dictionaries, pass in ``collections.OrderedDict``. + :param bool retain_collection_types: Do not convert to ``list`` when + encountering an attribute whose type is ``tuple`` or ``set``. Only + meaningful if ``recurse`` is ``True``. + :param Optional[callable] value_serializer: A hook that is called for every + attribute or dict key/value. It receives the current instance, field + and value and must return the (updated) value. The hook is run *after* + the optional *filter* has been applied. + + :rtype: return type of *dict_factory* + + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. versionadded:: 16.0.0 *dict_factory* + .. versionadded:: 16.1.0 *retain_collection_types* + .. versionadded:: 20.3.0 *value_serializer* + """ + attrs = fields(inst.__class__) + rv = dict_factory() + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + + if value_serializer is not None: + v = value_serializer(inst, a, v) + + if recurse is True: + if has(v.__class__): + rv[a.name] = asdict( + v, + True, + filter, + dict_factory, + retain_collection_types, + value_serializer, + ) + elif isinstance(v, (tuple, list, set, frozenset)): + cf = v.__class__ if retain_collection_types is True else list + rv[a.name] = cf( + [ + _asdict_anything( + i, + filter, + dict_factory, + retain_collection_types, + value_serializer, + ) + for i in v + ] + ) + elif isinstance(v, dict): + df = dict_factory + rv[a.name] = df( + ( + _asdict_anything( + kk, + filter, + df, + retain_collection_types, + value_serializer, + ), + _asdict_anything( + vv, + filter, + df, + retain_collection_types, + value_serializer, + ), + ) + for kk, vv in iteritems(v) + ) + else: + rv[a.name] = v + else: + rv[a.name] = v + return rv + + +def _asdict_anything( + val, + filter, + dict_factory, + retain_collection_types, + value_serializer, +): + """ + ``asdict`` only works on attrs instances, this works on anything. + """ + if getattr(val.__class__, "__attrs_attrs__", None) is not None: + # Attrs class. + rv = asdict( + val, + True, + filter, + dict_factory, + retain_collection_types, + value_serializer, + ) + elif isinstance(val, (tuple, list, set, frozenset)): + cf = val.__class__ if retain_collection_types is True else list + rv = cf( + [ + _asdict_anything( + i, + filter, + dict_factory, + retain_collection_types, + value_serializer, + ) + for i in val + ] + ) + elif isinstance(val, dict): + df = dict_factory + rv = df( + ( + _asdict_anything( + kk, filter, df, retain_collection_types, value_serializer + ), + _asdict_anything( + vv, filter, df, retain_collection_types, value_serializer + ), + ) + for kk, vv in iteritems(val) + ) + else: + rv = val + if value_serializer is not None: + rv = value_serializer(None, None, rv) + + return rv + + +def astuple( + inst, + recurse=True, + filter=None, + tuple_factory=tuple, + retain_collection_types=False, +): + """ + Return the ``attrs`` attribute values of *inst* as a tuple. + + Optionally recurse into other ``attrs``-decorated classes. + + :param inst: Instance of an ``attrs``-decorated class. + :param bool recurse: Recurse into classes that are also + ``attrs``-decorated. + :param callable filter: A callable whose return code determines whether an + attribute or element is included (``True``) or dropped (``False``). Is + called with the `attr.Attribute` as the first argument and the + value as the second argument. + :param callable tuple_factory: A callable to produce tuples from. For + example, to produce lists instead of tuples. + :param bool retain_collection_types: Do not convert to ``list`` + or ``dict`` when encountering an attribute which type is + ``tuple``, ``dict`` or ``set``. Only meaningful if ``recurse`` is + ``True``. + + :rtype: return type of *tuple_factory* + + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. versionadded:: 16.2.0 + """ + attrs = fields(inst.__class__) + rv = [] + retain = retain_collection_types # Very long. :/ + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + if recurse is True: + if has(v.__class__): + rv.append( + astuple( + v, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + ) + elif isinstance(v, (tuple, list, set, frozenset)): + cf = v.__class__ if retain is True else list + rv.append( + cf( + [ + astuple( + j, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(j.__class__) + else j + for j in v + ] + ) + ) + elif isinstance(v, dict): + df = v.__class__ if retain is True else dict + rv.append( + df( + ( + astuple( + kk, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(kk.__class__) + else kk, + astuple( + vv, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(vv.__class__) + else vv, + ) + for kk, vv in iteritems(v) + ) + ) + else: + rv.append(v) + else: + rv.append(v) + + return rv if tuple_factory is list else tuple_factory(rv) + + +def has(cls): + """ + Check whether *cls* is a class with ``attrs`` attributes. + + :param type cls: Class to introspect. + :raise TypeError: If *cls* is not a class. + + :rtype: bool + """ + return getattr(cls, "__attrs_attrs__", None) is not None + + +def assoc(inst, **changes): + """ + Copy *inst* and apply *changes*. + + :param inst: Instance of a class with ``attrs`` attributes. + :param changes: Keyword changes in the new copy. + + :return: A copy of inst with *changes* incorporated. + + :raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't + be found on *cls*. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. deprecated:: 17.1.0 + Use `evolve` instead. + """ + import warnings + + warnings.warn( + "assoc is deprecated and will be removed after 2018/01.", + DeprecationWarning, + stacklevel=2, + ) + new = copy.copy(inst) + attrs = fields(inst.__class__) + for k, v in iteritems(changes): + a = getattr(attrs, k, NOTHING) + if a is NOTHING: + raise AttrsAttributeNotFoundError( + "{k} is not an attrs attribute on {cl}.".format( + k=k, cl=new.__class__ + ) + ) + _obj_setattr(new, k, v) + return new + + +def evolve(inst, **changes): + """ + Create a new instance, based on *inst* with *changes* applied. + + :param inst: Instance of a class with ``attrs`` attributes. + :param changes: Keyword changes in the new copy. + + :return: A copy of inst with *changes* incorporated. + + :raise TypeError: If *attr_name* couldn't be found in the class + ``__init__``. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + .. versionadded:: 17.1.0 + """ + cls = inst.__class__ + attrs = fields(cls) + for a in attrs: + if not a.init: + continue + attr_name = a.name # To deal with private attributes. + init_name = attr_name if attr_name[0] != "_" else attr_name[1:] + if init_name not in changes: + changes[init_name] = getattr(inst, attr_name) + + return cls(**changes) + + +def resolve_types(cls, globalns=None, localns=None, attribs=None): + """ + Resolve any strings and forward annotations in type annotations. + + This is only required if you need concrete types in `Attribute`'s *type* + field. In other words, you don't need to resolve your types if you only + use them for static type checking. + + With no arguments, names will be looked up in the module in which the class + was created. If this is not what you want, e.g. if the name only exists + inside a method, you may pass *globalns* or *localns* to specify other + dictionaries in which to look up these names. See the docs of + `typing.get_type_hints` for more details. + + :param type cls: Class to resolve. + :param Optional[dict] globalns: Dictionary containing global variables. + :param Optional[dict] localns: Dictionary containing local variables. + :param Optional[list] attribs: List of attribs for the given class. + This is necessary when calling from inside a ``field_transformer`` + since *cls* is not an ``attrs`` class yet. + + :raise TypeError: If *cls* is not a class. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class and you didn't pass any attribs. + :raise NameError: If types cannot be resolved because of missing variables. + + :returns: *cls* so you can use this function also as a class decorator. + Please note that you have to apply it **after** `attr.s`. That means + the decorator has to come in the line **before** `attr.s`. + + .. versionadded:: 20.1.0 + .. versionadded:: 21.1.0 *attribs* + + """ + try: + # Since calling get_type_hints is expensive we cache whether we've + # done it already. + cls.__attrs_types_resolved__ + except AttributeError: + import typing + + hints = typing.get_type_hints(cls, globalns=globalns, localns=localns) + for field in fields(cls) if attribs is None else attribs: + if field.name in hints: + # Since fields have been frozen we must work around it. + _obj_setattr(field, "type", hints[field.name]) + cls.__attrs_types_resolved__ = True + + # Return the class so you can use it as a decorator too. + return cls diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/_make.py b/flask-app/venv/lib/python3.8/site-packages/attr/_make.py new file mode 100644 index 000000000..a1912b123 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/_make.py @@ -0,0 +1,3052 @@ +from __future__ import absolute_import, division, print_function + +import copy +import inspect +import linecache +import sys +import threading +import uuid +import warnings + +from operator import itemgetter + +from . import _config, setters +from ._compat import ( + PY2, + PYPY, + isclass, + iteritems, + metadata_proxy, + new_class, + ordered_dict, + set_closure_cell, +) +from .exceptions import ( + DefaultAlreadySetError, + FrozenInstanceError, + NotAnAttrsClassError, + PythonTooOldError, + UnannotatedAttributeError, +) + + +if not PY2: + import typing + + +# This is used at least twice, so cache it here. +_obj_setattr = object.__setattr__ +_init_converter_pat = "__attr_converter_%s" +_init_factory_pat = "__attr_factory_{}" +_tuple_property_pat = ( + " {attr_name} = _attrs_property(_attrs_itemgetter({index}))" +) +_classvar_prefixes = ( + "typing.ClassVar", + "t.ClassVar", + "ClassVar", + "typing_extensions.ClassVar", +) +# we don't use a double-underscore prefix because that triggers +# name mangling when trying to create a slot for the field +# (when slots=True) +_hash_cache_field = "_attrs_cached_hash" + +_empty_metadata_singleton = metadata_proxy({}) + +# Unique object for unequivocal getattr() defaults. +_sentinel = object() + + +class _Nothing(object): + """ + Sentinel class to indicate the lack of a value when ``None`` is ambiguous. + + ``_Nothing`` is a singleton. There is only ever one of it. + + .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False. + """ + + _singleton = None + + def __new__(cls): + if _Nothing._singleton is None: + _Nothing._singleton = super(_Nothing, cls).__new__(cls) + return _Nothing._singleton + + def __repr__(self): + return "NOTHING" + + def __bool__(self): + return False + + def __len__(self): + return 0 # __bool__ for Python 2 + + +NOTHING = _Nothing() +""" +Sentinel to indicate the lack of a value when ``None`` is ambiguous. +""" + + +class _CacheHashWrapper(int): + """ + An integer subclass that pickles / copies as None + + This is used for non-slots classes with ``cache_hash=True``, to avoid + serializing a potentially (even likely) invalid hash value. Since ``None`` + is the default value for uncalculated hashes, whenever this is copied, + the copy's value for the hash should automatically reset. + + See GH #613 for more details. + """ + + if PY2: + # For some reason `type(None)` isn't callable in Python 2, but we don't + # actually need a constructor for None objects, we just need any + # available function that returns None. + def __reduce__(self, _none_constructor=getattr, _args=(0, "", None)): + return _none_constructor, _args + + else: + + def __reduce__(self, _none_constructor=type(None), _args=()): + return _none_constructor, _args + + +def attrib( + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=None, + init=True, + metadata=None, + type=None, + converter=None, + factory=None, + kw_only=False, + eq=None, + order=None, + on_setattr=None, +): + """ + Create a new attribute on a class. + + .. warning:: + + Does *not* do anything unless the class is also decorated with + `attr.s`! + + :param default: A value that is used if an ``attrs``-generated ``__init__`` + is used and no value is passed while instantiating or the attribute is + excluded using ``init=False``. + + If the value is an instance of `Factory`, its callable will be + used to construct a new value (useful for mutable data types like lists + or dicts). + + If a default is not set (or set manually to `attr.NOTHING`), a value + *must* be supplied when instantiating; otherwise a `TypeError` + will be raised. + + The default can also be set using decorator notation as shown below. + + :type default: Any value + + :param callable factory: Syntactic sugar for + ``default=attr.Factory(factory)``. + + :param validator: `callable` that is called by ``attrs``-generated + ``__init__`` methods after the instance has been initialized. They + receive the initialized instance, the `Attribute`, and the + passed value. + + The return value is *not* inspected so the validator has to throw an + exception itself. + + If a `list` is passed, its items are treated as validators and must + all pass. + + Validators can be globally disabled and re-enabled using + `get_run_validators`. + + The validator can also be set using decorator notation as shown below. + + :type validator: `callable` or a `list` of `callable`\\ s. + + :param repr: Include this attribute in the generated ``__repr__`` + method. If ``True``, include the attribute; if ``False``, omit it. By + default, the built-in ``repr()`` function is used. To override how the + attribute value is formatted, pass a ``callable`` that takes a single + value and returns a string. Note that the resulting string is used + as-is, i.e. it will be used directly *instead* of calling ``repr()`` + (the default). + :type repr: a `bool` or a `callable` to use a custom function. + + :param eq: If ``True`` (default), include this attribute in the + generated ``__eq__`` and ``__ne__`` methods that check two instances + for equality. To override how the attribute value is compared, + pass a ``callable`` that takes a single value and returns the value + to be compared. + :type eq: a `bool` or a `callable`. + + :param order: If ``True`` (default), include this attributes in the + generated ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods. + To override how the attribute value is ordered, + pass a ``callable`` that takes a single value and returns the value + to be ordered. + :type order: a `bool` or a `callable`. + + :param cmp: Setting *cmp* is equivalent to setting *eq* and *order* to the + same value. Must not be mixed with *eq* or *order*. + :type cmp: a `bool` or a `callable`. + + :param Optional[bool] hash: Include this attribute in the generated + ``__hash__`` method. If ``None`` (default), mirror *eq*'s value. This + is the correct behavior according the Python spec. Setting this value + to anything else than ``None`` is *discouraged*. + :param bool init: Include this attribute in the generated ``__init__`` + method. It is possible to set this to ``False`` and set a default + value. In that case this attributed is unconditionally initialized + with the specified default value or factory. + :param callable converter: `callable` that is called by + ``attrs``-generated ``__init__`` methods to convert attribute's value + to the desired format. It is given the passed-in value, and the + returned value will be used as the new value of the attribute. The + value is converted before being passed to the validator, if any. + :param metadata: An arbitrary mapping, to be used by third-party + components. See `extending_metadata`. + :param type: The type of the attribute. In Python 3.6 or greater, the + preferred method to specify the type is using a variable annotation + (see `PEP 526 `_). + This argument is provided for backward compatibility. + Regardless of the approach used, the type will be stored on + ``Attribute.type``. + + Please note that ``attrs`` doesn't do anything with this metadata by + itself. You can use it as part of your own code or for + `static type checking `. + :param kw_only: Make this attribute keyword-only (Python 3+) + in the generated ``__init__`` (if ``init`` is ``False``, this + parameter is ignored). + :param on_setattr: Allows to overwrite the *on_setattr* setting from + `attr.s`. If left `None`, the *on_setattr* value from `attr.s` is used. + Set to `attr.setters.NO_OP` to run **no** `setattr` hooks for this + attribute -- regardless of the setting in `attr.s`. + :type on_setattr: `callable`, or a list of callables, or `None`, or + `attr.setters.NO_OP` + + .. versionadded:: 15.2.0 *convert* + .. versionadded:: 16.3.0 *metadata* + .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. + .. versionchanged:: 17.1.0 + *hash* is ``None`` and therefore mirrors *eq* by default. + .. versionadded:: 17.3.0 *type* + .. deprecated:: 17.4.0 *convert* + .. versionadded:: 17.4.0 *converter* as a replacement for the deprecated + *convert* to achieve consistency with other noun-based arguments. + .. versionadded:: 18.1.0 + ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``. + .. versionadded:: 18.2.0 *kw_only* + .. versionchanged:: 19.2.0 *convert* keyword argument removed. + .. versionchanged:: 19.2.0 *repr* also accepts a custom callable. + .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. + .. versionadded:: 19.2.0 *eq* and *order* + .. versionadded:: 20.1.0 *on_setattr* + .. versionchanged:: 20.3.0 *kw_only* backported to Python 2 + .. versionchanged:: 21.1.0 + *eq*, *order*, and *cmp* also accept a custom callable + .. versionchanged:: 21.1.0 *cmp* undeprecated + """ + eq, eq_key, order, order_key = _determine_attrib_eq_order( + cmp, eq, order, True + ) + + if hash is not None and hash is not True and hash is not False: + raise TypeError( + "Invalid value for hash. Must be True, False, or None." + ) + + if factory is not None: + if default is not NOTHING: + raise ValueError( + "The `default` and `factory` arguments are mutually " + "exclusive." + ) + if not callable(factory): + raise ValueError("The `factory` argument must be a callable.") + default = Factory(factory) + + if metadata is None: + metadata = {} + + # Apply syntactic sugar by auto-wrapping. + if isinstance(on_setattr, (list, tuple)): + on_setattr = setters.pipe(*on_setattr) + + if validator and isinstance(validator, (list, tuple)): + validator = and_(*validator) + + if converter and isinstance(converter, (list, tuple)): + converter = pipe(*converter) + + return _CountingAttr( + default=default, + validator=validator, + repr=repr, + cmp=None, + hash=hash, + init=init, + converter=converter, + metadata=metadata, + type=type, + kw_only=kw_only, + eq=eq, + eq_key=eq_key, + order=order, + order_key=order_key, + on_setattr=on_setattr, + ) + + +def _compile_and_eval(script, globs, locs=None, filename=""): + """ + "Exec" the script with the given global (globs) and local (locs) variables. + """ + bytecode = compile(script, filename, "exec") + eval(bytecode, globs, locs) + + +def _make_method(name, script, filename, globs=None): + """ + Create the method with the script given and return the method object. + """ + locs = {} + if globs is None: + globs = {} + + _compile_and_eval(script, globs, locs, filename) + + # In order of debuggers like PDB being able to step through the code, + # we add a fake linecache entry. + linecache.cache[filename] = ( + len(script), + None, + script.splitlines(True), + filename, + ) + + return locs[name] + + +def _make_attr_tuple_class(cls_name, attr_names): + """ + Create a tuple subclass to hold `Attribute`s for an `attrs` class. + + The subclass is a bare tuple with properties for names. + + class MyClassAttributes(tuple): + __slots__ = () + x = property(itemgetter(0)) + """ + attr_class_name = "{}Attributes".format(cls_name) + attr_class_template = [ + "class {}(tuple):".format(attr_class_name), + " __slots__ = ()", + ] + if attr_names: + for i, attr_name in enumerate(attr_names): + attr_class_template.append( + _tuple_property_pat.format(index=i, attr_name=attr_name) + ) + else: + attr_class_template.append(" pass") + globs = {"_attrs_itemgetter": itemgetter, "_attrs_property": property} + _compile_and_eval("\n".join(attr_class_template), globs) + return globs[attr_class_name] + + +# Tuple class for extracted attributes from a class definition. +# `base_attrs` is a subset of `attrs`. +_Attributes = _make_attr_tuple_class( + "_Attributes", + [ + # all attributes to build dunder methods for + "attrs", + # attributes that have been inherited + "base_attrs", + # map inherited attributes to their originating classes + "base_attrs_map", + ], +) + + +def _is_class_var(annot): + """ + Check whether *annot* is a typing.ClassVar. + + The string comparison hack is used to avoid evaluating all string + annotations which would put attrs-based classes at a performance + disadvantage compared to plain old classes. + """ + annot = str(annot) + + # Annotation can be quoted. + if annot.startswith(("'", '"')) and annot.endswith(("'", '"')): + annot = annot[1:-1] + + return annot.startswith(_classvar_prefixes) + + +def _has_own_attribute(cls, attrib_name): + """ + Check whether *cls* defines *attrib_name* (and doesn't just inherit it). + + Requires Python 3. + """ + attr = getattr(cls, attrib_name, _sentinel) + if attr is _sentinel: + return False + + for base_cls in cls.__mro__[1:]: + a = getattr(base_cls, attrib_name, None) + if attr is a: + return False + + return True + + +def _get_annotations(cls): + """ + Get annotations for *cls*. + """ + if _has_own_attribute(cls, "__annotations__"): + return cls.__annotations__ + + return {} + + +def _counter_getter(e): + """ + Key function for sorting to avoid re-creating a lambda for every class. + """ + return e[1].counter + + +def _collect_base_attrs(cls, taken_attr_names): + """ + Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. + """ + base_attrs = [] + base_attr_map = {} # A dictionary of base attrs to their classes. + + # Traverse the MRO and collect attributes. + for base_cls in reversed(cls.__mro__[1:-1]): + for a in getattr(base_cls, "__attrs_attrs__", []): + if a.inherited or a.name in taken_attr_names: + continue + + a = a.evolve(inherited=True) + base_attrs.append(a) + base_attr_map[a.name] = base_cls + + # For each name, only keep the freshest definition i.e. the furthest at the + # back. base_attr_map is fine because it gets overwritten with every new + # instance. + filtered = [] + seen = set() + for a in reversed(base_attrs): + if a.name in seen: + continue + filtered.insert(0, a) + seen.add(a.name) + + return filtered, base_attr_map + + +def _collect_base_attrs_broken(cls, taken_attr_names): + """ + Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. + + N.B. *taken_attr_names* will be mutated. + + Adhere to the old incorrect behavior. + + Notably it collects from the front and considers inherited attributes which + leads to the buggy behavior reported in #428. + """ + base_attrs = [] + base_attr_map = {} # A dictionary of base attrs to their classes. + + # Traverse the MRO and collect attributes. + for base_cls in cls.__mro__[1:-1]: + for a in getattr(base_cls, "__attrs_attrs__", []): + if a.name in taken_attr_names: + continue + + a = a.evolve(inherited=True) + taken_attr_names.add(a.name) + base_attrs.append(a) + base_attr_map[a.name] = base_cls + + return base_attrs, base_attr_map + + +def _transform_attrs( + cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer +): + """ + Transform all `_CountingAttr`s on a class into `Attribute`s. + + If *these* is passed, use that and don't look for them on the class. + + *collect_by_mro* is True, collect them in the correct MRO order, otherwise + use the old -- incorrect -- order. See #428. + + Return an `_Attributes`. + """ + cd = cls.__dict__ + anns = _get_annotations(cls) + + if these is not None: + ca_list = [(name, ca) for name, ca in iteritems(these)] + + if not isinstance(these, ordered_dict): + ca_list.sort(key=_counter_getter) + elif auto_attribs is True: + ca_names = { + name + for name, attr in cd.items() + if isinstance(attr, _CountingAttr) + } + ca_list = [] + annot_names = set() + for attr_name, type in anns.items(): + if _is_class_var(type): + continue + annot_names.add(attr_name) + a = cd.get(attr_name, NOTHING) + + if not isinstance(a, _CountingAttr): + if a is NOTHING: + a = attrib() + else: + a = attrib(default=a) + ca_list.append((attr_name, a)) + + unannotated = ca_names - annot_names + if len(unannotated) > 0: + raise UnannotatedAttributeError( + "The following `attr.ib`s lack a type annotation: " + + ", ".join( + sorted(unannotated, key=lambda n: cd.get(n).counter) + ) + + "." + ) + else: + ca_list = sorted( + ( + (name, attr) + for name, attr in cd.items() + if isinstance(attr, _CountingAttr) + ), + key=lambda e: e[1].counter, + ) + + own_attrs = [ + Attribute.from_counting_attr( + name=attr_name, ca=ca, type=anns.get(attr_name) + ) + for attr_name, ca in ca_list + ] + + if collect_by_mro: + base_attrs, base_attr_map = _collect_base_attrs( + cls, {a.name for a in own_attrs} + ) + else: + base_attrs, base_attr_map = _collect_base_attrs_broken( + cls, {a.name for a in own_attrs} + ) + + attr_names = [a.name for a in base_attrs + own_attrs] + + AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) + + if kw_only: + own_attrs = [a.evolve(kw_only=True) for a in own_attrs] + base_attrs = [a.evolve(kw_only=True) for a in base_attrs] + + attrs = AttrsClass(base_attrs + own_attrs) + + # Mandatory vs non-mandatory attr order only matters when they are part of + # the __init__ signature and when they aren't kw_only (which are moved to + # the end and can be mandatory or non-mandatory in any order, as they will + # be specified as keyword args anyway). Check the order of those attrs: + had_default = False + for a in (a for a in attrs if a.init is not False and a.kw_only is False): + if had_default is True and a.default is NOTHING: + raise ValueError( + "No mandatory attributes allowed after an attribute with a " + "default value or factory. Attribute in question: %r" % (a,) + ) + + if had_default is False and a.default is not NOTHING: + had_default = True + + if field_transformer is not None: + attrs = field_transformer(cls, attrs) + return _Attributes((attrs, base_attrs, base_attr_map)) + + +if PYPY: + + def _frozen_setattrs(self, name, value): + """ + Attached to frozen classes as __setattr__. + """ + if isinstance(self, BaseException) and name in ( + "__cause__", + "__context__", + ): + BaseException.__setattr__(self, name, value) + return + + raise FrozenInstanceError() + + +else: + + def _frozen_setattrs(self, name, value): + """ + Attached to frozen classes as __setattr__. + """ + raise FrozenInstanceError() + + +def _frozen_delattrs(self, name): + """ + Attached to frozen classes as __delattr__. + """ + raise FrozenInstanceError() + + +class _ClassBuilder(object): + """ + Iteratively build *one* class. + """ + + __slots__ = ( + "_attr_names", + "_attrs", + "_base_attr_map", + "_base_names", + "_cache_hash", + "_cls", + "_cls_dict", + "_delete_attribs", + "_frozen", + "_has_pre_init", + "_has_post_init", + "_is_exc", + "_on_setattr", + "_slots", + "_weakref_slot", + "_has_own_setattr", + "_has_custom_setattr", + ) + + def __init__( + self, + cls, + these, + slots, + frozen, + weakref_slot, + getstate_setstate, + auto_attribs, + kw_only, + cache_hash, + is_exc, + collect_by_mro, + on_setattr, + has_custom_setattr, + field_transformer, + ): + attrs, base_attrs, base_map = _transform_attrs( + cls, + these, + auto_attribs, + kw_only, + collect_by_mro, + field_transformer, + ) + + self._cls = cls + self._cls_dict = dict(cls.__dict__) if slots else {} + self._attrs = attrs + self._base_names = set(a.name for a in base_attrs) + self._base_attr_map = base_map + self._attr_names = tuple(a.name for a in attrs) + self._slots = slots + self._frozen = frozen + self._weakref_slot = weakref_slot + self._cache_hash = cache_hash + self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False)) + self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) + self._delete_attribs = not bool(these) + self._is_exc = is_exc + self._on_setattr = on_setattr + + self._has_custom_setattr = has_custom_setattr + self._has_own_setattr = False + + self._cls_dict["__attrs_attrs__"] = self._attrs + + if frozen: + self._cls_dict["__setattr__"] = _frozen_setattrs + self._cls_dict["__delattr__"] = _frozen_delattrs + + self._has_own_setattr = True + + if getstate_setstate: + ( + self._cls_dict["__getstate__"], + self._cls_dict["__setstate__"], + ) = self._make_getstate_setstate() + + def __repr__(self): + return "<_ClassBuilder(cls={cls})>".format(cls=self._cls.__name__) + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used after calling this method. + """ + if self._slots is True: + return self._create_slots_class() + else: + return self._patch_original_class() + + def _patch_original_class(self): + """ + Apply accumulated methods and return the class. + """ + cls = self._cls + base_names = self._base_names + + # Clean class of attribute definitions (`attr.ib()`s). + if self._delete_attribs: + for name in self._attr_names: + if ( + name not in base_names + and getattr(cls, name, _sentinel) is not _sentinel + ): + try: + delattr(cls, name) + except AttributeError: + # This can happen if a base class defines a class + # variable and we want to set an attribute with the + # same name by using only a type annotation. + pass + + # Attach our dunder methods. + for name, value in self._cls_dict.items(): + setattr(cls, name, value) + + # If we've inherited an attrs __setattr__ and don't write our own, + # reset it to object's. + if not self._has_own_setattr and getattr( + cls, "__attrs_own_setattr__", False + ): + cls.__attrs_own_setattr__ = False + + if not self._has_custom_setattr: + cls.__setattr__ = object.__setattr__ + + return cls + + def _create_slots_class(self): + """ + Build and return a new class with a `__slots__` attribute. + """ + cd = { + k: v + for k, v in iteritems(self._cls_dict) + if k not in tuple(self._attr_names) + ("__dict__", "__weakref__") + } + + # If our class doesn't have its own implementation of __setattr__ + # (either from the user or by us), check the bases, if one of them has + # an attrs-made __setattr__, that needs to be reset. We don't walk the + # MRO because we only care about our immediate base classes. + # XXX: This can be confused by subclassing a slotted attrs class with + # XXX: a non-attrs class and subclass the resulting class with an attrs + # XXX: class. See `test_slotted_confused` for details. For now that's + # XXX: OK with us. + if not self._has_own_setattr: + cd["__attrs_own_setattr__"] = False + + if not self._has_custom_setattr: + for base_cls in self._cls.__bases__: + if base_cls.__dict__.get("__attrs_own_setattr__", False): + cd["__setattr__"] = object.__setattr__ + break + + # Traverse the MRO to collect existing slots + # and check for an existing __weakref__. + existing_slots = dict() + weakref_inherited = False + for base_cls in self._cls.__mro__[1:-1]: + if base_cls.__dict__.get("__weakref__", None) is not None: + weakref_inherited = True + existing_slots.update( + { + name: getattr(base_cls, name) + for name in getattr(base_cls, "__slots__", []) + } + ) + + base_names = set(self._base_names) + + names = self._attr_names + if ( + self._weakref_slot + and "__weakref__" not in getattr(self._cls, "__slots__", ()) + and "__weakref__" not in names + and not weakref_inherited + ): + names += ("__weakref__",) + + # We only add the names of attributes that aren't inherited. + # Setting __slots__ to inherited attributes wastes memory. + slot_names = [name for name in names if name not in base_names] + # There are slots for attributes from current class + # that are defined in parent classes. + # As their descriptors may be overriden by a child class, + # we collect them here and update the class dict + reused_slots = { + slot: slot_descriptor + for slot, slot_descriptor in iteritems(existing_slots) + if slot in slot_names + } + slot_names = [name for name in slot_names if name not in reused_slots] + cd.update(reused_slots) + if self._cache_hash: + slot_names.append(_hash_cache_field) + cd["__slots__"] = tuple(slot_names) + + qualname = getattr(self._cls, "__qualname__", None) + if qualname is not None: + cd["__qualname__"] = qualname + + # Create new class based on old class and our methods. + cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd) + + # The following is a fix for + # https://github.com/python-attrs/attrs/issues/102. On Python 3, + # if a method mentions `__class__` or uses the no-arg super(), the + # compiler will bake a reference to the class in the method itself + # as `method.__closure__`. Since we replace the class with a + # clone, we rewrite these references so it keeps working. + for item in cls.__dict__.values(): + if isinstance(item, (classmethod, staticmethod)): + # Class- and staticmethods hide their functions inside. + # These might need to be rewritten as well. + closure_cells = getattr(item.__func__, "__closure__", None) + elif isinstance(item, property): + # Workaround for property `super()` shortcut (PY3-only). + # There is no universal way for other descriptors. + closure_cells = getattr(item.fget, "__closure__", None) + else: + closure_cells = getattr(item, "__closure__", None) + + if not closure_cells: # Catch None or the empty list. + continue + for cell in closure_cells: + try: + match = cell.cell_contents is self._cls + except ValueError: # ValueError: Cell is empty + pass + else: + if match: + set_closure_cell(cell, cls) + + return cls + + def add_repr(self, ns): + self._cls_dict["__repr__"] = self._add_method_dunders( + _make_repr(self._attrs, ns=ns) + ) + return self + + def add_str(self): + repr = self._cls_dict.get("__repr__") + if repr is None: + raise ValueError( + "__str__ can only be generated if a __repr__ exists." + ) + + def __str__(self): + return self.__repr__() + + self._cls_dict["__str__"] = self._add_method_dunders(__str__) + return self + + def _make_getstate_setstate(self): + """ + Create custom __setstate__ and __getstate__ methods. + """ + # __weakref__ is not writable. + state_attr_names = tuple( + an for an in self._attr_names if an != "__weakref__" + ) + + def slots_getstate(self): + """ + Automatically created by attrs. + """ + return tuple(getattr(self, name) for name in state_attr_names) + + hash_caching_enabled = self._cache_hash + + def slots_setstate(self, state): + """ + Automatically created by attrs. + """ + __bound_setattr = _obj_setattr.__get__(self, Attribute) + for name, value in zip(state_attr_names, state): + __bound_setattr(name, value) + + # The hash code cache is not included when the object is + # serialized, but it still needs to be initialized to None to + # indicate that the first call to __hash__ should be a cache + # miss. + if hash_caching_enabled: + __bound_setattr(_hash_cache_field, None) + + return slots_getstate, slots_setstate + + def make_unhashable(self): + self._cls_dict["__hash__"] = None + return self + + def add_hash(self): + self._cls_dict["__hash__"] = self._add_method_dunders( + _make_hash( + self._cls, + self._attrs, + frozen=self._frozen, + cache_hash=self._cache_hash, + ) + ) + + return self + + def add_init(self): + self._cls_dict["__init__"] = self._add_method_dunders( + _make_init( + self._cls, + self._attrs, + self._has_pre_init, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + self._on_setattr is not None + and self._on_setattr is not setters.NO_OP, + attrs_init=False, + ) + ) + + return self + + def add_attrs_init(self): + self._cls_dict["__attrs_init__"] = self._add_method_dunders( + _make_init( + self._cls, + self._attrs, + self._has_pre_init, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + self._on_setattr is not None + and self._on_setattr is not setters.NO_OP, + attrs_init=True, + ) + ) + + return self + + def add_eq(self): + cd = self._cls_dict + + cd["__eq__"] = self._add_method_dunders( + _make_eq(self._cls, self._attrs) + ) + cd["__ne__"] = self._add_method_dunders(_make_ne()) + + return self + + def add_order(self): + cd = self._cls_dict + + cd["__lt__"], cd["__le__"], cd["__gt__"], cd["__ge__"] = ( + self._add_method_dunders(meth) + for meth in _make_order(self._cls, self._attrs) + ) + + return self + + def add_setattr(self): + if self._frozen: + return self + + sa_attrs = {} + for a in self._attrs: + on_setattr = a.on_setattr or self._on_setattr + if on_setattr and on_setattr is not setters.NO_OP: + sa_attrs[a.name] = a, on_setattr + + if not sa_attrs: + return self + + if self._has_custom_setattr: + # We need to write a __setattr__ but there already is one! + raise ValueError( + "Can't combine custom __setattr__ with on_setattr hooks." + ) + + # docstring comes from _add_method_dunders + def __setattr__(self, name, val): + try: + a, hook = sa_attrs[name] + except KeyError: + nval = val + else: + nval = hook(self, a, val) + + _obj_setattr(self, name, nval) + + self._cls_dict["__attrs_own_setattr__"] = True + self._cls_dict["__setattr__"] = self._add_method_dunders(__setattr__) + self._has_own_setattr = True + + return self + + def _add_method_dunders(self, method): + """ + Add __module__ and __qualname__ to a *method* if possible. + """ + try: + method.__module__ = self._cls.__module__ + except AttributeError: + pass + + try: + method.__qualname__ = ".".join( + (self._cls.__qualname__, method.__name__) + ) + except AttributeError: + pass + + try: + method.__doc__ = "Method generated by attrs for class %s." % ( + self._cls.__qualname__, + ) + except AttributeError: + pass + + return method + + +_CMP_DEPRECATION = ( + "The usage of `cmp` is deprecated and will be removed on or after " + "2021-06-01. Please use `eq` and `order` instead." +) + + +def _determine_attrs_eq_order(cmp, eq, order, default_eq): + """ + Validate the combination of *cmp*, *eq*, and *order*. Derive the effective + values of eq and order. If *eq* is None, set it to *default_eq*. + """ + if cmp is not None and any((eq is not None, order is not None)): + raise ValueError("Don't mix `cmp` with `eq' and `order`.") + + # cmp takes precedence due to bw-compatibility. + if cmp is not None: + return cmp, cmp + + # If left None, equality is set to the specified default and ordering + # mirrors equality. + if eq is None: + eq = default_eq + + if order is None: + order = eq + + if eq is False and order is True: + raise ValueError("`order` can only be True if `eq` is True too.") + + return eq, order + + +def _determine_attrib_eq_order(cmp, eq, order, default_eq): + """ + Validate the combination of *cmp*, *eq*, and *order*. Derive the effective + values of eq and order. If *eq* is None, set it to *default_eq*. + """ + if cmp is not None and any((eq is not None, order is not None)): + raise ValueError("Don't mix `cmp` with `eq' and `order`.") + + def decide_callable_or_boolean(value): + """ + Decide whether a key function is used. + """ + if callable(value): + value, key = True, value + else: + key = None + return value, key + + # cmp takes precedence due to bw-compatibility. + if cmp is not None: + cmp, cmp_key = decide_callable_or_boolean(cmp) + return cmp, cmp_key, cmp, cmp_key + + # If left None, equality is set to the specified default and ordering + # mirrors equality. + if eq is None: + eq, eq_key = default_eq, None + else: + eq, eq_key = decide_callable_or_boolean(eq) + + if order is None: + order, order_key = eq, eq_key + else: + order, order_key = decide_callable_or_boolean(order) + + if eq is False and order is True: + raise ValueError("`order` can only be True if `eq` is True too.") + + return eq, eq_key, order, order_key + + +def _determine_whether_to_implement( + cls, flag, auto_detect, dunders, default=True +): + """ + Check whether we should implement a set of methods for *cls*. + + *flag* is the argument passed into @attr.s like 'init', *auto_detect* the + same as passed into @attr.s and *dunders* is a tuple of attribute names + whose presence signal that the user has implemented it themselves. + + Return *default* if no reason for either for or against is found. + + auto_detect must be False on Python 2. + """ + if flag is True or flag is False: + return flag + + if flag is None and auto_detect is False: + return default + + # Logically, flag is None and auto_detect is True here. + for dunder in dunders: + if _has_own_attribute(cls, dunder): + return False + + return default + + +def attrs( + maybe_cls=None, + these=None, + repr_ns=None, + repr=None, + cmp=None, + hash=None, + init=None, + slots=False, + frozen=False, + weakref_slot=True, + str=False, + auto_attribs=False, + kw_only=False, + cache_hash=False, + auto_exc=False, + eq=None, + order=None, + auto_detect=False, + collect_by_mro=False, + getstate_setstate=None, + on_setattr=None, + field_transformer=None, +): + r""" + A class decorator that adds `dunder + `_\ -methods according to the + specified attributes using `attr.ib` or the *these* argument. + + :param these: A dictionary of name to `attr.ib` mappings. This is + useful to avoid the definition of your attributes within the class body + because you can't (e.g. if you want to add ``__repr__`` methods to + Django models) or don't want to. + + If *these* is not ``None``, ``attrs`` will *not* search the class body + for attributes and will *not* remove any attributes from it. + + If *these* is an ordered dict (`dict` on Python 3.6+, + `collections.OrderedDict` otherwise), the order is deduced from + the order of the attributes inside *these*. Otherwise the order + of the definition of the attributes is used. + + :type these: `dict` of `str` to `attr.ib` + + :param str repr_ns: When using nested classes, there's no way in Python 2 + to automatically detect that. Therefore it's possible to set the + namespace explicitly for a more meaningful ``repr`` output. + :param bool auto_detect: Instead of setting the *init*, *repr*, *eq*, + *order*, and *hash* arguments explicitly, assume they are set to + ``True`` **unless any** of the involved methods for one of the + arguments is implemented in the *current* class (i.e. it is *not* + inherited from some base class). + + So for example by implementing ``__eq__`` on a class yourself, + ``attrs`` will deduce ``eq=False`` and will create *neither* + ``__eq__`` *nor* ``__ne__`` (but Python classes come with a sensible + ``__ne__`` by default, so it *should* be enough to only implement + ``__eq__`` in most cases). + + .. warning:: + + If you prevent ``attrs`` from creating the ordering methods for you + (``order=False``, e.g. by implementing ``__le__``), it becomes + *your* responsibility to make sure its ordering is sound. The best + way is to use the `functools.total_ordering` decorator. + + + Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*, + *cmp*, or *hash* overrides whatever *auto_detect* would determine. + + *auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises + a `PythonTooOldError`. + + :param bool repr: Create a ``__repr__`` method with a human readable + representation of ``attrs`` attributes.. + :param bool str: Create a ``__str__`` method that is identical to + ``__repr__``. This is usually not necessary except for + `Exception`\ s. + :param Optional[bool] eq: If ``True`` or ``None`` (default), add ``__eq__`` + and ``__ne__`` methods that check two instances for equality. + + They compare the instances as if they were tuples of their ``attrs`` + attributes if and only if the types of both classes are *identical*! + :param Optional[bool] order: If ``True``, add ``__lt__``, ``__le__``, + ``__gt__``, and ``__ge__`` methods that behave like *eq* above and + allow instances to be ordered. If ``None`` (default) mirror value of + *eq*. + :param Optional[bool] cmp: Setting *cmp* is equivalent to setting *eq* + and *order* to the same value. Must not be mixed with *eq* or *order*. + :param Optional[bool] hash: If ``None`` (default), the ``__hash__`` method + is generated according how *eq* and *frozen* are set. + + 1. If *both* are True, ``attrs`` will generate a ``__hash__`` for you. + 2. If *eq* is True and *frozen* is False, ``__hash__`` will be set to + None, marking it unhashable (which it is). + 3. If *eq* is False, ``__hash__`` will be left untouched meaning the + ``__hash__`` method of the base class will be used (if base class is + ``object``, this means it will fall back to id-based hashing.). + + Although not recommended, you can decide for yourself and force + ``attrs`` to create one (e.g. if the class is immutable even though you + didn't freeze it programmatically) by passing ``True`` or not. Both of + these cases are rather special and should be used carefully. + + See our documentation on `hashing`, Python's documentation on + `object.__hash__`, and the `GitHub issue that led to the default \ + behavior `_ for more + details. + :param bool init: Create a ``__init__`` method that initializes the + ``attrs`` attributes. Leading underscores are stripped for the argument + name. If a ``__attrs_pre_init__`` method exists on the class, it will + be called before the class is initialized. If a ``__attrs_post_init__`` + method exists on the class, it will be called after the class is fully + initialized. + + If ``init`` is ``False``, an ``__attrs_init__`` method will be + injected instead. This allows you to define a custom ``__init__`` + method that can do pre-init work such as ``super().__init__()``, + and then call ``__attrs_init__()`` and ``__attrs_post_init__()``. + :param bool slots: Create a `slotted class ` that's more + memory-efficient. Slotted classes are generally superior to the default + dict classes, but have some gotchas you should know about, so we + encourage you to read the `glossary entry `. + :param bool frozen: Make instances immutable after initialization. If + someone attempts to modify a frozen instance, + `attr.exceptions.FrozenInstanceError` is raised. + + .. note:: + + 1. This is achieved by installing a custom ``__setattr__`` method + on your class, so you can't implement your own. + + 2. True immutability is impossible in Python. + + 3. This *does* have a minor a runtime performance `impact + ` when initializing new instances. In other words: + ``__init__`` is slightly slower with ``frozen=True``. + + 4. If a class is frozen, you cannot modify ``self`` in + ``__attrs_post_init__`` or a self-written ``__init__``. You can + circumvent that limitation by using + ``object.__setattr__(self, "attribute_name", value)``. + + 5. Subclasses of a frozen class are frozen too. + + :param bool weakref_slot: Make instances weak-referenceable. This has no + effect unless ``slots`` is also enabled. + :param bool auto_attribs: If ``True``, collect `PEP 526`_-annotated + attributes (Python 3.6 and later only) from the class body. + + In this case, you **must** annotate every field. If ``attrs`` + encounters a field that is set to an `attr.ib` but lacks a type + annotation, an `attr.exceptions.UnannotatedAttributeError` is + raised. Use ``field_name: typing.Any = attr.ib(...)`` if you don't + want to set a type. + + If you assign a value to those attributes (e.g. ``x: int = 42``), that + value becomes the default value like if it were passed using + ``attr.ib(default=42)``. Passing an instance of `Factory` also + works as expected in most cases (see warning below). + + Attributes annotated as `typing.ClassVar`, and attributes that are + neither annotated nor set to an `attr.ib` are **ignored**. + + .. warning:: + For features that use the attribute name to create decorators (e.g. + `validators `), you still *must* assign `attr.ib` to + them. Otherwise Python will either not find the name or try to use + the default value to call e.g. ``validator`` on it. + + These errors can be quite confusing and probably the most common bug + report on our bug tracker. + + .. _`PEP 526`: https://www.python.org/dev/peps/pep-0526/ + :param bool kw_only: Make all attributes keyword-only (Python 3+) + in the generated ``__init__`` (if ``init`` is ``False``, this + parameter is ignored). + :param bool cache_hash: Ensure that the object's hash code is computed + only once and stored on the object. If this is set to ``True``, + hashing must be either explicitly or implicitly enabled for this + class. If the hash code is cached, avoid any reassignments of + fields involved in hash code computation or mutations of the objects + those fields point to after object creation. If such changes occur, + the behavior of the object's hash code is undefined. + :param bool auto_exc: If the class subclasses `BaseException` + (which implicitly includes any subclass of any exception), the + following happens to behave like a well-behaved Python exceptions + class: + + - the values for *eq*, *order*, and *hash* are ignored and the + instances compare and hash by the instance's ids (N.B. ``attrs`` will + *not* remove existing implementations of ``__hash__`` or the equality + methods. It just won't add own ones.), + - all attributes that are either passed into ``__init__`` or have a + default value are additionally available as a tuple in the ``args`` + attribute, + - the value of *str* is ignored leaving ``__str__`` to base classes. + :param bool collect_by_mro: Setting this to `True` fixes the way ``attrs`` + collects attributes from base classes. The default behavior is + incorrect in certain cases of multiple inheritance. It should be on by + default but is kept off for backward-compatability. + + See issue `#428 `_ for + more details. + + :param Optional[bool] getstate_setstate: + .. note:: + This is usually only interesting for slotted classes and you should + probably just set *auto_detect* to `True`. + + If `True`, ``__getstate__`` and + ``__setstate__`` are generated and attached to the class. This is + necessary for slotted classes to be pickleable. If left `None`, it's + `True` by default for slotted classes and ``False`` for dict classes. + + If *auto_detect* is `True`, and *getstate_setstate* is left `None`, + and **either** ``__getstate__`` or ``__setstate__`` is detected directly + on the class (i.e. not inherited), it is set to `False` (this is usually + what you want). + + :param on_setattr: A callable that is run whenever the user attempts to set + an attribute (either by assignment like ``i.x = 42`` or by using + `setattr` like ``setattr(i, "x", 42)``). It receives the same arguments + as validators: the instance, the attribute that is being modified, and + the new value. + + If no exception is raised, the attribute is set to the return value of + the callable. + + If a list of callables is passed, they're automatically wrapped in an + `attr.setters.pipe`. + + :param Optional[callable] field_transformer: + A function that is called with the original class object and all + fields right before ``attrs`` finalizes the class. You can use + this, e.g., to automatically add converters or validators to + fields based on their types. See `transform-fields` for more details. + + .. versionadded:: 16.0.0 *slots* + .. versionadded:: 16.1.0 *frozen* + .. versionadded:: 16.3.0 *str* + .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``. + .. versionchanged:: 17.1.0 + *hash* supports ``None`` as value which is also the default now. + .. versionadded:: 17.3.0 *auto_attribs* + .. versionchanged:: 18.1.0 + If *these* is passed, no attributes are deleted from the class body. + .. versionchanged:: 18.1.0 If *these* is ordered, the order is retained. + .. versionadded:: 18.2.0 *weakref_slot* + .. deprecated:: 18.2.0 + ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a + `DeprecationWarning` if the classes compared are subclasses of + each other. ``__eq`` and ``__ne__`` never tried to compared subclasses + to each other. + .. versionchanged:: 19.2.0 + ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now do not consider + subclasses comparable anymore. + .. versionadded:: 18.2.0 *kw_only* + .. versionadded:: 18.2.0 *cache_hash* + .. versionadded:: 19.1.0 *auto_exc* + .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. + .. versionadded:: 19.2.0 *eq* and *order* + .. versionadded:: 20.1.0 *auto_detect* + .. versionadded:: 20.1.0 *collect_by_mro* + .. versionadded:: 20.1.0 *getstate_setstate* + .. versionadded:: 20.1.0 *on_setattr* + .. versionadded:: 20.3.0 *field_transformer* + .. versionchanged:: 21.1.0 + ``init=False`` injects ``__attrs_init__`` + .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__`` + .. versionchanged:: 21.1.0 *cmp* undeprecated + """ + if auto_detect and PY2: + raise PythonTooOldError( + "auto_detect only works on Python 3 and later." + ) + + eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None) + hash_ = hash # work around the lack of nonlocal + + if isinstance(on_setattr, (list, tuple)): + on_setattr = setters.pipe(*on_setattr) + + def wrap(cls): + + if getattr(cls, "__class__", None) is None: + raise TypeError("attrs only works with new-style classes.") + + is_frozen = frozen or _has_frozen_base_class(cls) + is_exc = auto_exc is True and issubclass(cls, BaseException) + has_own_setattr = auto_detect and _has_own_attribute( + cls, "__setattr__" + ) + + if has_own_setattr and is_frozen: + raise ValueError("Can't freeze a class with a custom __setattr__.") + + builder = _ClassBuilder( + cls, + these, + slots, + is_frozen, + weakref_slot, + _determine_whether_to_implement( + cls, + getstate_setstate, + auto_detect, + ("__getstate__", "__setstate__"), + default=slots, + ), + auto_attribs, + kw_only, + cache_hash, + is_exc, + collect_by_mro, + on_setattr, + has_own_setattr, + field_transformer, + ) + if _determine_whether_to_implement( + cls, repr, auto_detect, ("__repr__",) + ): + builder.add_repr(repr_ns) + if str is True: + builder.add_str() + + eq = _determine_whether_to_implement( + cls, eq_, auto_detect, ("__eq__", "__ne__") + ) + if not is_exc and eq is True: + builder.add_eq() + if not is_exc and _determine_whether_to_implement( + cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__") + ): + builder.add_order() + + builder.add_setattr() + + if ( + hash_ is None + and auto_detect is True + and _has_own_attribute(cls, "__hash__") + ): + hash = False + else: + hash = hash_ + if hash is not True and hash is not False and hash is not None: + # Can't use `hash in` because 1 == True for example. + raise TypeError( + "Invalid value for hash. Must be True, False, or None." + ) + elif hash is False or (hash is None and eq is False) or is_exc: + # Don't do anything. Should fall back to __object__'s __hash__ + # which is by id. + if cache_hash: + raise TypeError( + "Invalid value for cache_hash. To use hash caching," + " hashing must be either explicitly or implicitly " + "enabled." + ) + elif hash is True or ( + hash is None and eq is True and is_frozen is True + ): + # Build a __hash__ if told so, or if it's safe. + builder.add_hash() + else: + # Raise TypeError on attempts to hash. + if cache_hash: + raise TypeError( + "Invalid value for cache_hash. To use hash caching," + " hashing must be either explicitly or implicitly " + "enabled." + ) + builder.make_unhashable() + + if _determine_whether_to_implement( + cls, init, auto_detect, ("__init__",) + ): + builder.add_init() + else: + builder.add_attrs_init() + if cache_hash: + raise TypeError( + "Invalid value for cache_hash. To use hash caching," + " init must be True." + ) + + return builder.build_class() + + # maybe_cls's type depends on the usage of the decorator. It's a class + # if it's used as `@attrs` but ``None`` if used as `@attrs()`. + if maybe_cls is None: + return wrap + else: + return wrap(maybe_cls) + + +_attrs = attrs +""" +Internal alias so we can use it in functions that take an argument called +*attrs*. +""" + + +if PY2: + + def _has_frozen_base_class(cls): + """ + Check whether *cls* has a frozen ancestor by looking at its + __setattr__. + """ + return ( + getattr(cls.__setattr__, "__module__", None) + == _frozen_setattrs.__module__ + and cls.__setattr__.__name__ == _frozen_setattrs.__name__ + ) + + +else: + + def _has_frozen_base_class(cls): + """ + Check whether *cls* has a frozen ancestor by looking at its + __setattr__. + """ + return cls.__setattr__ == _frozen_setattrs + + +def _generate_unique_filename(cls, func_name): + """ + Create a "filename" suitable for a function being generated. + """ + unique_id = uuid.uuid4() + extra = "" + count = 1 + + while True: + unique_filename = "".format( + func_name, + cls.__module__, + getattr(cls, "__qualname__", cls.__name__), + extra, + ) + # To handle concurrency we essentially "reserve" our spot in + # the linecache with a dummy line. The caller can then + # set this value correctly. + cache_line = (1, None, (str(unique_id),), unique_filename) + if ( + linecache.cache.setdefault(unique_filename, cache_line) + == cache_line + ): + return unique_filename + + # Looks like this spot is taken. Try again. + count += 1 + extra = "-{0}".format(count) + + +def _make_hash(cls, attrs, frozen, cache_hash): + attrs = tuple( + a for a in attrs if a.hash is True or (a.hash is None and a.eq is True) + ) + + tab = " " + + unique_filename = _generate_unique_filename(cls, "hash") + type_hash = hash(unique_filename) + + hash_def = "def __hash__(self" + hash_func = "hash((" + closing_braces = "))" + if not cache_hash: + hash_def += "):" + else: + if not PY2: + hash_def += ", *" + + hash_def += ( + ", _cache_wrapper=" + + "__import__('attr._make')._make._CacheHashWrapper):" + ) + hash_func = "_cache_wrapper(" + hash_func + closing_braces += ")" + + method_lines = [hash_def] + + def append_hash_computation_lines(prefix, indent): + """ + Generate the code for actually computing the hash code. + Below this will either be returned directly or used to compute + a value which is then cached, depending on the value of cache_hash + """ + + method_lines.extend( + [ + indent + prefix + hash_func, + indent + " %d," % (type_hash,), + ] + ) + + for a in attrs: + method_lines.append(indent + " self.%s," % a.name) + + method_lines.append(indent + " " + closing_braces) + + if cache_hash: + method_lines.append(tab + "if self.%s is None:" % _hash_cache_field) + if frozen: + append_hash_computation_lines( + "object.__setattr__(self, '%s', " % _hash_cache_field, tab * 2 + ) + method_lines.append(tab * 2 + ")") # close __setattr__ + else: + append_hash_computation_lines( + "self.%s = " % _hash_cache_field, tab * 2 + ) + method_lines.append(tab + "return self.%s" % _hash_cache_field) + else: + append_hash_computation_lines("return ", tab) + + script = "\n".join(method_lines) + return _make_method("__hash__", script, unique_filename) + + +def _add_hash(cls, attrs): + """ + Add a hash method to *cls*. + """ + cls.__hash__ = _make_hash(cls, attrs, frozen=False, cache_hash=False) + return cls + + +def _make_ne(): + """ + Create __ne__ method. + """ + + def __ne__(self, other): + """ + Check equality and either forward a NotImplemented or + return the result negated. + """ + result = self.__eq__(other) + if result is NotImplemented: + return NotImplemented + + return not result + + return __ne__ + + +def _make_eq(cls, attrs): + """ + Create __eq__ method for *cls* with *attrs*. + """ + attrs = [a for a in attrs if a.eq] + + unique_filename = _generate_unique_filename(cls, "eq") + lines = [ + "def __eq__(self, other):", + " if other.__class__ is not self.__class__:", + " return NotImplemented", + ] + + # We can't just do a big self.x = other.x and... clause due to + # irregularities like nan == nan is false but (nan,) == (nan,) is true. + globs = {} + if attrs: + lines.append(" return (") + others = [" ) == ("] + for a in attrs: + if a.eq_key: + cmp_name = "_%s_key" % (a.name,) + # Add the key function to the global namespace + # of the evaluated function. + globs[cmp_name] = a.eq_key + lines.append( + " %s(self.%s)," + % ( + cmp_name, + a.name, + ) + ) + others.append( + " %s(other.%s)," + % ( + cmp_name, + a.name, + ) + ) + else: + lines.append(" self.%s," % (a.name,)) + others.append(" other.%s," % (a.name,)) + + lines += others + [" )"] + else: + lines.append(" return True") + + script = "\n".join(lines) + + return _make_method("__eq__", script, unique_filename, globs) + + +def _make_order(cls, attrs): + """ + Create ordering methods for *cls* with *attrs*. + """ + attrs = [a for a in attrs if a.order] + + def attrs_to_tuple(obj): + """ + Save us some typing. + """ + return tuple( + key(value) if key else value + for value, key in ( + (getattr(obj, a.name), a.order_key) for a in attrs + ) + ) + + def __lt__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) < attrs_to_tuple(other) + + return NotImplemented + + def __le__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) <= attrs_to_tuple(other) + + return NotImplemented + + def __gt__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) > attrs_to_tuple(other) + + return NotImplemented + + def __ge__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) >= attrs_to_tuple(other) + + return NotImplemented + + return __lt__, __le__, __gt__, __ge__ + + +def _add_eq(cls, attrs=None): + """ + Add equality methods to *cls* with *attrs*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + cls.__eq__ = _make_eq(cls, attrs) + cls.__ne__ = _make_ne() + + return cls + + +_already_repring = threading.local() + + +def _make_repr(attrs, ns): + """ + Make a repr method that includes relevant *attrs*, adding *ns* to the full + name. + """ + + # Figure out which attributes to include, and which function to use to + # format them. The a.repr value can be either bool or a custom callable. + attr_names_with_reprs = tuple( + (a.name, repr if a.repr is True else a.repr) + for a in attrs + if a.repr is not False + ) + + def __repr__(self): + """ + Automatically created by attrs. + """ + try: + working_set = _already_repring.working_set + except AttributeError: + working_set = set() + _already_repring.working_set = working_set + + if id(self) in working_set: + return "..." + real_cls = self.__class__ + if ns is None: + qualname = getattr(real_cls, "__qualname__", None) + if qualname is not None: + class_name = qualname.rsplit(">.", 1)[-1] + else: + class_name = real_cls.__name__ + else: + class_name = ns + "." + real_cls.__name__ + + # Since 'self' remains on the stack (i.e.: strongly referenced) for the + # duration of this call, it's safe to depend on id(...) stability, and + # not need to track the instance and therefore worry about properties + # like weakref- or hash-ability. + working_set.add(id(self)) + try: + result = [class_name, "("] + first = True + for name, attr_repr in attr_names_with_reprs: + if first: + first = False + else: + result.append(", ") + result.extend( + (name, "=", attr_repr(getattr(self, name, NOTHING))) + ) + return "".join(result) + ")" + finally: + working_set.remove(id(self)) + + return __repr__ + + +def _add_repr(cls, ns=None, attrs=None): + """ + Add a repr method to *cls*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + cls.__repr__ = _make_repr(attrs, ns) + return cls + + +def fields(cls): + """ + Return the tuple of ``attrs`` attributes for a class. + + The tuple also allows accessing the fields by their names (see below for + examples). + + :param type cls: Class to introspect. + + :raise TypeError: If *cls* is not a class. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + :rtype: tuple (with name accessors) of `attr.Attribute` + + .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields + by name. + """ + if not isclass(cls): + raise TypeError("Passed object must be a class.") + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: + raise NotAnAttrsClassError( + "{cls!r} is not an attrs-decorated class.".format(cls=cls) + ) + return attrs + + +def fields_dict(cls): + """ + Return an ordered dictionary of ``attrs`` attributes for a class, whose + keys are the attribute names. + + :param type cls: Class to introspect. + + :raise TypeError: If *cls* is not a class. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class. + + :rtype: an ordered dict where keys are attribute names and values are + `attr.Attribute`\\ s. This will be a `dict` if it's + naturally ordered like on Python 3.6+ or an + :class:`~collections.OrderedDict` otherwise. + + .. versionadded:: 18.1.0 + """ + if not isclass(cls): + raise TypeError("Passed object must be a class.") + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: + raise NotAnAttrsClassError( + "{cls!r} is not an attrs-decorated class.".format(cls=cls) + ) + return ordered_dict(((a.name, a) for a in attrs)) + + +def validate(inst): + """ + Validate all attributes on *inst* that have a validator. + + Leaves all exceptions through. + + :param inst: Instance of a class with ``attrs`` attributes. + """ + if _config._run_validators is False: + return + + for a in fields(inst.__class__): + v = a.validator + if v is not None: + v(inst, a, getattr(inst, a.name)) + + +def _is_slot_cls(cls): + return "__slots__" in cls.__dict__ + + +def _is_slot_attr(a_name, base_attr_map): + """ + Check if the attribute name comes from a slot class. + """ + return a_name in base_attr_map and _is_slot_cls(base_attr_map[a_name]) + + +def _make_init( + cls, + attrs, + pre_init, + post_init, + frozen, + slots, + cache_hash, + base_attr_map, + is_exc, + has_global_on_setattr, + attrs_init, +): + if frozen and has_global_on_setattr: + raise ValueError("Frozen classes can't use on_setattr.") + + needs_cached_setattr = cache_hash or frozen + filtered_attrs = [] + attr_dict = {} + for a in attrs: + if not a.init and a.default is NOTHING: + continue + + filtered_attrs.append(a) + attr_dict[a.name] = a + + if a.on_setattr is not None: + if frozen is True: + raise ValueError("Frozen classes can't use on_setattr.") + + needs_cached_setattr = True + elif ( + has_global_on_setattr and a.on_setattr is not setters.NO_OP + ) or _is_slot_attr(a.name, base_attr_map): + needs_cached_setattr = True + + unique_filename = _generate_unique_filename(cls, "init") + + script, globs, annotations = _attrs_to_init_script( + filtered_attrs, + frozen, + slots, + pre_init, + post_init, + cache_hash, + base_attr_map, + is_exc, + needs_cached_setattr, + has_global_on_setattr, + attrs_init, + ) + if cls.__module__ in sys.modules: + # This makes typing.get_type_hints(CLS.__init__) resolve string types. + globs.update(sys.modules[cls.__module__].__dict__) + + globs.update({"NOTHING": NOTHING, "attr_dict": attr_dict}) + + if needs_cached_setattr: + # Save the lookup overhead in __init__ if we need to circumvent + # setattr hooks. + globs["_cached_setattr"] = _obj_setattr + + init = _make_method( + "__attrs_init__" if attrs_init else "__init__", + script, + unique_filename, + globs, + ) + init.__annotations__ = annotations + + return init + + +def _setattr(attr_name, value_var, has_on_setattr): + """ + Use the cached object.setattr to set *attr_name* to *value_var*. + """ + return "_setattr('%s', %s)" % (attr_name, value_var) + + +def _setattr_with_converter(attr_name, value_var, has_on_setattr): + """ + Use the cached object.setattr to set *attr_name* to *value_var*, but run + its converter first. + """ + return "_setattr('%s', %s(%s))" % ( + attr_name, + _init_converter_pat % (attr_name,), + value_var, + ) + + +def _assign(attr_name, value, has_on_setattr): + """ + Unless *attr_name* has an on_setattr hook, use normal assignment. Otherwise + relegate to _setattr. + """ + if has_on_setattr: + return _setattr(attr_name, value, True) + + return "self.%s = %s" % (attr_name, value) + + +def _assign_with_converter(attr_name, value_var, has_on_setattr): + """ + Unless *attr_name* has an on_setattr hook, use normal assignment after + conversion. Otherwise relegate to _setattr_with_converter. + """ + if has_on_setattr: + return _setattr_with_converter(attr_name, value_var, True) + + return "self.%s = %s(%s)" % ( + attr_name, + _init_converter_pat % (attr_name,), + value_var, + ) + + +if PY2: + + def _unpack_kw_only_py2(attr_name, default=None): + """ + Unpack *attr_name* from _kw_only dict. + """ + if default is not None: + arg_default = ", %s" % default + else: + arg_default = "" + return "%s = _kw_only.pop('%s'%s)" % ( + attr_name, + attr_name, + arg_default, + ) + + def _unpack_kw_only_lines_py2(kw_only_args): + """ + Unpack all *kw_only_args* from _kw_only dict and handle errors. + + Given a list of strings "{attr_name}" and "{attr_name}={default}" + generates list of lines of code that pop attrs from _kw_only dict and + raise TypeError similar to builtin if required attr is missing or + extra key is passed. + + >>> print("\n".join(_unpack_kw_only_lines_py2(["a", "b=42"]))) + try: + a = _kw_only.pop('a') + b = _kw_only.pop('b', 42) + except KeyError as _key_error: + raise TypeError( + ... + if _kw_only: + raise TypeError( + ... + """ + lines = ["try:"] + lines.extend( + " " + _unpack_kw_only_py2(*arg.split("=")) + for arg in kw_only_args + ) + lines += """\ +except KeyError as _key_error: + raise TypeError( + '__init__() missing required keyword-only argument: %s' % _key_error + ) +if _kw_only: + raise TypeError( + '__init__() got an unexpected keyword argument %r' + % next(iter(_kw_only)) + ) +""".split( + "\n" + ) + return lines + + +def _attrs_to_init_script( + attrs, + frozen, + slots, + pre_init, + post_init, + cache_hash, + base_attr_map, + is_exc, + needs_cached_setattr, + has_global_on_setattr, + attrs_init, +): + """ + Return a script of an initializer for *attrs* and a dict of globals. + + The globals are expected by the generated script. + + If *frozen* is True, we cannot set the attributes directly so we use + a cached ``object.__setattr__``. + """ + lines = [] + if pre_init: + lines.append("self.__attrs_pre_init__()") + + if needs_cached_setattr: + lines.append( + # Circumvent the __setattr__ descriptor to save one lookup per + # assignment. + # Note _setattr will be used again below if cache_hash is True + "_setattr = _cached_setattr.__get__(self, self.__class__)" + ) + + if frozen is True: + if slots is True: + fmt_setter = _setattr + fmt_setter_with_converter = _setattr_with_converter + else: + # Dict frozen classes assign directly to __dict__. + # But only if the attribute doesn't come from an ancestor slot + # class. + # Note _inst_dict will be used again below if cache_hash is True + lines.append("_inst_dict = self.__dict__") + + def fmt_setter(attr_name, value_var, has_on_setattr): + if _is_slot_attr(attr_name, base_attr_map): + return _setattr(attr_name, value_var, has_on_setattr) + + return "_inst_dict['%s'] = %s" % (attr_name, value_var) + + def fmt_setter_with_converter( + attr_name, value_var, has_on_setattr + ): + if has_on_setattr or _is_slot_attr(attr_name, base_attr_map): + return _setattr_with_converter( + attr_name, value_var, has_on_setattr + ) + + return "_inst_dict['%s'] = %s(%s)" % ( + attr_name, + _init_converter_pat % (attr_name,), + value_var, + ) + + else: + # Not frozen. + fmt_setter = _assign + fmt_setter_with_converter = _assign_with_converter + + args = [] + kw_only_args = [] + attrs_to_validate = [] + + # This is a dictionary of names to validator and converter callables. + # Injecting this into __init__ globals lets us avoid lookups. + names_for_globals = {} + annotations = {"return": None} + + for a in attrs: + if a.validator: + attrs_to_validate.append(a) + + attr_name = a.name + has_on_setattr = a.on_setattr is not None or ( + a.on_setattr is not setters.NO_OP and has_global_on_setattr + ) + arg_name = a.name.lstrip("_") + + has_factory = isinstance(a.default, Factory) + if has_factory and a.default.takes_self: + maybe_self = "self" + else: + maybe_self = "" + + if a.init is False: + if has_factory: + init_factory_name = _init_factory_pat.format(a.name) + if a.converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + init_factory_name + "(%s)" % (maybe_self,), + has_on_setattr, + ) + ) + conv_name = _init_converter_pat % (a.name,) + names_for_globals[conv_name] = a.converter + else: + lines.append( + fmt_setter( + attr_name, + init_factory_name + "(%s)" % (maybe_self,), + has_on_setattr, + ) + ) + names_for_globals[init_factory_name] = a.default.factory + else: + if a.converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + "attr_dict['%s'].default" % (attr_name,), + has_on_setattr, + ) + ) + conv_name = _init_converter_pat % (a.name,) + names_for_globals[conv_name] = a.converter + else: + lines.append( + fmt_setter( + attr_name, + "attr_dict['%s'].default" % (attr_name,), + has_on_setattr, + ) + ) + elif a.default is not NOTHING and not has_factory: + arg = "%s=attr_dict['%s'].default" % (arg_name, attr_name) + if a.kw_only: + kw_only_args.append(arg) + else: + args.append(arg) + + if a.converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr + ) + ) + names_for_globals[ + _init_converter_pat % (a.name,) + ] = a.converter + else: + lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) + + elif has_factory: + arg = "%s=NOTHING" % (arg_name,) + if a.kw_only: + kw_only_args.append(arg) + else: + args.append(arg) + lines.append("if %s is not NOTHING:" % (arg_name,)) + + init_factory_name = _init_factory_pat.format(a.name) + if a.converter is not None: + lines.append( + " " + + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr + ) + ) + lines.append("else:") + lines.append( + " " + + fmt_setter_with_converter( + attr_name, + init_factory_name + "(" + maybe_self + ")", + has_on_setattr, + ) + ) + names_for_globals[ + _init_converter_pat % (a.name,) + ] = a.converter + else: + lines.append( + " " + fmt_setter(attr_name, arg_name, has_on_setattr) + ) + lines.append("else:") + lines.append( + " " + + fmt_setter( + attr_name, + init_factory_name + "(" + maybe_self + ")", + has_on_setattr, + ) + ) + names_for_globals[init_factory_name] = a.default.factory + else: + if a.kw_only: + kw_only_args.append(arg_name) + else: + args.append(arg_name) + + if a.converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr + ) + ) + names_for_globals[ + _init_converter_pat % (a.name,) + ] = a.converter + else: + lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) + + if a.init is True: + if a.type is not None and a.converter is None: + annotations[arg_name] = a.type + elif a.converter is not None and not PY2: + # Try to get the type from the converter. + sig = None + try: + sig = inspect.signature(a.converter) + except (ValueError, TypeError): # inspect failed + pass + if sig: + sig_params = list(sig.parameters.values()) + if ( + sig_params + and sig_params[0].annotation + is not inspect.Parameter.empty + ): + annotations[arg_name] = sig_params[0].annotation + + if attrs_to_validate: # we can skip this if there are no validators. + names_for_globals["_config"] = _config + lines.append("if _config._run_validators is True:") + for a in attrs_to_validate: + val_name = "__attr_validator_" + a.name + attr_name = "__attr_" + a.name + lines.append( + " %s(self, %s, self.%s)" % (val_name, attr_name, a.name) + ) + names_for_globals[val_name] = a.validator + names_for_globals[attr_name] = a + + if post_init: + lines.append("self.__attrs_post_init__()") + + # because this is set only after __attrs_post_init is called, a crash + # will result if post-init tries to access the hash code. This seemed + # preferable to setting this beforehand, in which case alteration to + # field values during post-init combined with post-init accessing the + # hash code would result in silent bugs. + if cache_hash: + if frozen: + if slots: + # if frozen and slots, then _setattr defined above + init_hash_cache = "_setattr('%s', %s)" + else: + # if frozen and not slots, then _inst_dict defined above + init_hash_cache = "_inst_dict['%s'] = %s" + else: + init_hash_cache = "self.%s = %s" + lines.append(init_hash_cache % (_hash_cache_field, "None")) + + # For exceptions we rely on BaseException.__init__ for proper + # initialization. + if is_exc: + vals = ",".join("self." + a.name for a in attrs if a.init) + + lines.append("BaseException.__init__(self, %s)" % (vals,)) + + args = ", ".join(args) + if kw_only_args: + if PY2: + lines = _unpack_kw_only_lines_py2(kw_only_args) + lines + + args += "%s**_kw_only" % (", " if args else "",) # leading comma + else: + args += "%s*, %s" % ( + ", " if args else "", # leading comma + ", ".join(kw_only_args), # kw_only args + ) + return ( + """\ +def {init_name}(self, {args}): + {lines} +""".format( + init_name=("__attrs_init__" if attrs_init else "__init__"), + args=args, + lines="\n ".join(lines) if lines else "pass", + ), + names_for_globals, + annotations, + ) + + +class Attribute(object): + """ + *Read-only* representation of an attribute. + + Instances of this class are frequently used for introspection purposes + like: + + - `fields` returns a tuple of them. + - Validators get them passed as the first argument. + - The *field transformer* hook receives a list of them. + + :attribute name: The name of the attribute. + :attribute inherited: Whether or not that attribute has been inherited from + a base class. + + Plus *all* arguments of `attr.ib` (except for ``factory`` + which is only syntactic sugar for ``default=Factory(...)``. + + .. versionadded:: 20.1.0 *inherited* + .. versionadded:: 20.1.0 *on_setattr* + .. versionchanged:: 20.2.0 *inherited* is not taken into account for + equality checks and hashing anymore. + .. versionadded:: 21.1.0 *eq_key* and *order_key* + + For the full version history of the fields, see `attr.ib`. + """ + + __slots__ = ( + "name", + "default", + "validator", + "repr", + "eq", + "eq_key", + "order", + "order_key", + "hash", + "init", + "metadata", + "type", + "converter", + "kw_only", + "inherited", + "on_setattr", + ) + + def __init__( + self, + name, + default, + validator, + repr, + cmp, # XXX: unused, remove along with other cmp code. + hash, + init, + inherited, + metadata=None, + type=None, + converter=None, + kw_only=False, + eq=None, + eq_key=None, + order=None, + order_key=None, + on_setattr=None, + ): + eq, eq_key, order, order_key = _determine_attrib_eq_order( + cmp, eq_key or eq, order_key or order, True + ) + + # Cache this descriptor here to speed things up later. + bound_setattr = _obj_setattr.__get__(self, Attribute) + + # Despite the big red warning, people *do* instantiate `Attribute` + # themselves. + bound_setattr("name", name) + bound_setattr("default", default) + bound_setattr("validator", validator) + bound_setattr("repr", repr) + bound_setattr("eq", eq) + bound_setattr("eq_key", eq_key) + bound_setattr("order", order) + bound_setattr("order_key", order_key) + bound_setattr("hash", hash) + bound_setattr("init", init) + bound_setattr("converter", converter) + bound_setattr( + "metadata", + ( + metadata_proxy(metadata) + if metadata + else _empty_metadata_singleton + ), + ) + bound_setattr("type", type) + bound_setattr("kw_only", kw_only) + bound_setattr("inherited", inherited) + bound_setattr("on_setattr", on_setattr) + + def __setattr__(self, name, value): + raise FrozenInstanceError() + + @classmethod + def from_counting_attr(cls, name, ca, type=None): + # type holds the annotated value. deal with conflicts: + if type is None: + type = ca.type + elif ca.type is not None: + raise ValueError( + "Type annotation and type argument cannot both be present" + ) + inst_dict = { + k: getattr(ca, k) + for k in Attribute.__slots__ + if k + not in ( + "name", + "validator", + "default", + "type", + "inherited", + ) # exclude methods and deprecated alias + } + return cls( + name=name, + validator=ca._validator, + default=ca._default, + type=type, + cmp=None, + inherited=False, + **inst_dict + ) + + @property + def cmp(self): + """ + Simulate the presence of a cmp attribute and warn. + """ + warnings.warn(_CMP_DEPRECATION, DeprecationWarning, stacklevel=2) + + return self.eq and self.order + + # Don't use attr.evolve since fields(Attribute) doesn't work + def evolve(self, **changes): + """ + Copy *self* and apply *changes*. + + This works similarly to `attr.evolve` but that function does not work + with ``Attribute``. + + It is mainly meant to be used for `transform-fields`. + + .. versionadded:: 20.3.0 + """ + new = copy.copy(self) + + new._setattrs(changes.items()) + + return new + + # Don't use _add_pickle since fields(Attribute) doesn't work + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple( + getattr(self, name) if name != "metadata" else dict(self.metadata) + for name in self.__slots__ + ) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + self._setattrs(zip(self.__slots__, state)) + + def _setattrs(self, name_values_pairs): + bound_setattr = _obj_setattr.__get__(self, Attribute) + for name, value in name_values_pairs: + if name != "metadata": + bound_setattr(name, value) + else: + bound_setattr( + name, + metadata_proxy(value) + if value + else _empty_metadata_singleton, + ) + + +_a = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=(name != "metadata"), + init=True, + inherited=False, + ) + for name in Attribute.__slots__ +] + +Attribute = _add_hash( + _add_eq( + _add_repr(Attribute, attrs=_a), + attrs=[a for a in _a if a.name != "inherited"], + ), + attrs=[a for a in _a if a.hash and a.name != "inherited"], +) + + +class _CountingAttr(object): + """ + Intermediate representation of attributes that uses a counter to preserve + the order in which the attributes have been defined. + + *Internal* data structure of the attrs library. Running into is most + likely the result of a bug like a forgotten `@attr.s` decorator. + """ + + __slots__ = ( + "counter", + "_default", + "repr", + "eq", + "eq_key", + "order", + "order_key", + "hash", + "init", + "metadata", + "_validator", + "converter", + "type", + "kw_only", + "on_setattr", + ) + __attrs_attrs__ = tuple( + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=True, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ) + for name in ( + "counter", + "_default", + "repr", + "eq", + "order", + "hash", + "init", + "on_setattr", + ) + ) + ( + Attribute( + name="metadata", + default=None, + validator=None, + repr=True, + cmp=None, + hash=False, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ), + ) + cls_counter = 0 + + def __init__( + self, + default, + validator, + repr, + cmp, + hash, + init, + converter, + metadata, + type, + kw_only, + eq, + eq_key, + order, + order_key, + on_setattr, + ): + _CountingAttr.cls_counter += 1 + self.counter = _CountingAttr.cls_counter + self._default = default + self._validator = validator + self.converter = converter + self.repr = repr + self.eq = eq + self.eq_key = eq_key + self.order = order + self.order_key = order_key + self.hash = hash + self.init = init + self.metadata = metadata + self.type = type + self.kw_only = kw_only + self.on_setattr = on_setattr + + def validator(self, meth): + """ + Decorator that adds *meth* to the list of validators. + + Returns *meth* unchanged. + + .. versionadded:: 17.1.0 + """ + if self._validator is None: + self._validator = meth + else: + self._validator = and_(self._validator, meth) + return meth + + def default(self, meth): + """ + Decorator that allows to set the default for an attribute. + + Returns *meth* unchanged. + + :raises DefaultAlreadySetError: If default has been set before. + + .. versionadded:: 17.1.0 + """ + if self._default is not NOTHING: + raise DefaultAlreadySetError() + + self._default = Factory(meth, takes_self=True) + + return meth + + +_CountingAttr = _add_eq(_add_repr(_CountingAttr)) + + +class Factory(object): + """ + Stores a factory callable. + + If passed as the default value to `attr.ib`, the factory is used to + generate a new value. + + :param callable factory: A callable that takes either none or exactly one + mandatory positional argument depending on *takes_self*. + :param bool takes_self: Pass the partially initialized instance that is + being initialized as a positional argument. + + .. versionadded:: 17.1.0 *takes_self* + """ + + __slots__ = ("factory", "takes_self") + + def __init__(self, factory, takes_self=False): + """ + `Factory` is part of the default machinery so if we want a default + value here, we have to implement it ourselves. + """ + self.factory = factory + self.takes_self = takes_self + + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple(getattr(self, name) for name in self.__slots__) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + for name, value in zip(self.__slots__, state): + setattr(self, name, value) + + +_f = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=True, + init=True, + inherited=False, + ) + for name in Factory.__slots__ +] + +Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f) + + +def make_class(name, attrs, bases=(object,), **attributes_arguments): + """ + A quick way to create a new class called *name* with *attrs*. + + :param str name: The name for the new class. + + :param attrs: A list of names or a dictionary of mappings of names to + attributes. + + If *attrs* is a list or an ordered dict (`dict` on Python 3.6+, + `collections.OrderedDict` otherwise), the order is deduced from + the order of the names or attributes inside *attrs*. Otherwise the + order of the definition of the attributes is used. + :type attrs: `list` or `dict` + + :param tuple bases: Classes that the new class will subclass. + + :param attributes_arguments: Passed unmodified to `attr.s`. + + :return: A new class with *attrs*. + :rtype: type + + .. versionadded:: 17.1.0 *bases* + .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained. + """ + if isinstance(attrs, dict): + cls_dict = attrs + elif isinstance(attrs, (list, tuple)): + cls_dict = dict((a, attrib()) for a in attrs) + else: + raise TypeError("attrs argument must be a dict or a list.") + + pre_init = cls_dict.pop("__attrs_pre_init__", None) + post_init = cls_dict.pop("__attrs_post_init__", None) + user_init = cls_dict.pop("__init__", None) + + body = {} + if pre_init is not None: + body["__attrs_pre_init__"] = pre_init + if post_init is not None: + body["__attrs_post_init__"] = post_init + if user_init is not None: + body["__init__"] = user_init + + type_ = new_class(name, bases, {}, lambda ns: ns.update(body)) + + # For pickling to work, the __module__ variable needs to be set to the + # frame where the class is created. Bypass this step in environments where + # sys._getframe is not defined (Jython for example) or sys._getframe is not + # defined for arguments greater than 0 (IronPython). + try: + type_.__module__ = sys._getframe(1).f_globals.get( + "__name__", "__main__" + ) + except (AttributeError, ValueError): + pass + + # We do it here for proper warnings with meaningful stacklevel. + cmp = attributes_arguments.pop("cmp", None) + ( + attributes_arguments["eq"], + attributes_arguments["order"], + ) = _determine_attrs_eq_order( + cmp, + attributes_arguments.get("eq"), + attributes_arguments.get("order"), + True, + ) + + return _attrs(these=cls_dict, **attributes_arguments)(type_) + + +# These are required by within this module so we define them here and merely +# import into .validators / .converters. + + +@attrs(slots=True, hash=True) +class _AndValidator(object): + """ + Compose many validators to a single one. + """ + + _validators = attrib() + + def __call__(self, inst, attr, value): + for v in self._validators: + v(inst, attr, value) + + +def and_(*validators): + """ + A validator that composes multiple validators into one. + + When called on a value, it runs all wrapped validators. + + :param callables validators: Arbitrary number of validators. + + .. versionadded:: 17.1.0 + """ + vals = [] + for validator in validators: + vals.extend( + validator._validators + if isinstance(validator, _AndValidator) + else [validator] + ) + + return _AndValidator(tuple(vals)) + + +def pipe(*converters): + """ + A converter that composes multiple converters into one. + + When called on a value, it runs all wrapped converters, returning the + *last* value. + + Type annotations will be inferred from the wrapped converters', if + they have any. + + :param callables converters: Arbitrary number of converters. + + .. versionadded:: 20.1.0 + """ + + def pipe_converter(val): + for converter in converters: + val = converter(val) + + return val + + if not PY2: + if not converters: + # If the converter list is empty, pipe_converter is the identity. + A = typing.TypeVar("A") + pipe_converter.__annotations__ = {"val": A, "return": A} + else: + # Get parameter type. + sig = None + try: + sig = inspect.signature(converters[0]) + except (ValueError, TypeError): # inspect failed + pass + if sig: + params = list(sig.parameters.values()) + if ( + params + and params[0].annotation is not inspect.Parameter.empty + ): + pipe_converter.__annotations__["val"] = params[ + 0 + ].annotation + # Get return type. + sig = None + try: + sig = inspect.signature(converters[-1]) + except (ValueError, TypeError): # inspect failed + pass + if sig and sig.return_annotation is not inspect.Signature().empty: + pipe_converter.__annotations__[ + "return" + ] = sig.return_annotation + + return pipe_converter diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/_next_gen.py b/flask-app/venv/lib/python3.8/site-packages/attr/_next_gen.py new file mode 100644 index 000000000..fab0af966 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/_next_gen.py @@ -0,0 +1,158 @@ +""" +These are Python 3.6+-only and keyword-only APIs that call `attr.s` and +`attr.ib` with different default values. +""" + +from functools import partial + +from attr.exceptions import UnannotatedAttributeError + +from . import setters +from ._make import NOTHING, _frozen_setattrs, attrib, attrs + + +def define( + maybe_cls=None, + *, + these=None, + repr=None, + hash=None, + init=None, + slots=True, + frozen=False, + weakref_slot=True, + str=False, + auto_attribs=None, + kw_only=False, + cache_hash=False, + auto_exc=True, + eq=None, + order=False, + auto_detect=True, + getstate_setstate=None, + on_setattr=None, + field_transformer=None, +): + r""" + The only behavioral differences are the handling of the *auto_attribs* + option: + + :param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves + exactly like `attr.s`. If left `None`, `attr.s` will try to guess: + + 1. If any attributes are annotated and no unannotated `attr.ib`\ s + are found, it assumes *auto_attribs=True*. + 2. Otherwise it assumes *auto_attribs=False* and tries to collect + `attr.ib`\ s. + + and that mutable classes (``frozen=False``) validate on ``__setattr__``. + + .. versionadded:: 20.1.0 + """ + + def do_it(cls, auto_attribs): + return attrs( + maybe_cls=cls, + these=these, + repr=repr, + hash=hash, + init=init, + slots=slots, + frozen=frozen, + weakref_slot=weakref_slot, + str=str, + auto_attribs=auto_attribs, + kw_only=kw_only, + cache_hash=cache_hash, + auto_exc=auto_exc, + eq=eq, + order=order, + auto_detect=auto_detect, + collect_by_mro=True, + getstate_setstate=getstate_setstate, + on_setattr=on_setattr, + field_transformer=field_transformer, + ) + + def wrap(cls): + """ + Making this a wrapper ensures this code runs during class creation. + + We also ensure that frozen-ness of classes is inherited. + """ + nonlocal frozen, on_setattr + + had_on_setattr = on_setattr not in (None, setters.NO_OP) + + # By default, mutable classes validate on setattr. + if frozen is False and on_setattr is None: + on_setattr = setters.validate + + # However, if we subclass a frozen class, we inherit the immutability + # and disable on_setattr. + for base_cls in cls.__bases__: + if base_cls.__setattr__ is _frozen_setattrs: + if had_on_setattr: + raise ValueError( + "Frozen classes can't use on_setattr " + "(frozen-ness was inherited)." + ) + + on_setattr = setters.NO_OP + break + + if auto_attribs is not None: + return do_it(cls, auto_attribs) + + try: + return do_it(cls, True) + except UnannotatedAttributeError: + return do_it(cls, False) + + # maybe_cls's type depends on the usage of the decorator. It's a class + # if it's used as `@attrs` but ``None`` if used as `@attrs()`. + if maybe_cls is None: + return wrap + else: + return wrap(maybe_cls) + + +mutable = define +frozen = partial(define, frozen=True, on_setattr=None) + + +def field( + *, + default=NOTHING, + validator=None, + repr=True, + hash=None, + init=True, + metadata=None, + converter=None, + factory=None, + kw_only=False, + eq=None, + order=None, + on_setattr=None, +): + """ + Identical to `attr.ib`, except keyword-only and with some arguments + removed. + + .. versionadded:: 20.1.0 + """ + return attrib( + default=default, + validator=validator, + repr=repr, + hash=hash, + init=init, + metadata=metadata, + converter=converter, + factory=factory, + kw_only=kw_only, + eq=eq, + order=order, + on_setattr=on_setattr, + ) diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/_version_info.py b/flask-app/venv/lib/python3.8/site-packages/attr/_version_info.py new file mode 100644 index 000000000..014e78a1b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/_version_info.py @@ -0,0 +1,85 @@ +from __future__ import absolute_import, division, print_function + +from functools import total_ordering + +from ._funcs import astuple +from ._make import attrib, attrs + + +@total_ordering +@attrs(eq=False, order=False, slots=True, frozen=True) +class VersionInfo(object): + """ + A version object that can be compared to tuple of length 1--4: + + >>> attr.VersionInfo(19, 1, 0, "final") <= (19, 2) + True + >>> attr.VersionInfo(19, 1, 0, "final") < (19, 1, 1) + True + >>> vi = attr.VersionInfo(19, 2, 0, "final") + >>> vi < (19, 1, 1) + False + >>> vi < (19,) + False + >>> vi == (19, 2,) + True + >>> vi == (19, 2, 1) + False + + .. versionadded:: 19.2 + """ + + year = attrib(type=int) + minor = attrib(type=int) + micro = attrib(type=int) + releaselevel = attrib(type=str) + + @classmethod + def _from_version_string(cls, s): + """ + Parse *s* and return a _VersionInfo. + """ + v = s.split(".") + if len(v) == 3: + v.append("final") + + return cls( + year=int(v[0]), minor=int(v[1]), micro=int(v[2]), releaselevel=v[3] + ) + + def _ensure_tuple(self, other): + """ + Ensure *other* is a tuple of a valid length. + + Returns a possibly transformed *other* and ourselves as a tuple of + the same length as *other*. + """ + + if self.__class__ is other.__class__: + other = astuple(other) + + if not isinstance(other, tuple): + raise NotImplementedError + + if not (1 <= len(other) <= 4): + raise NotImplementedError + + return astuple(self)[: len(other)], other + + def __eq__(self, other): + try: + us, them = self._ensure_tuple(other) + except NotImplementedError: + return NotImplemented + + return us == them + + def __lt__(self, other): + try: + us, them = self._ensure_tuple(other) + except NotImplementedError: + return NotImplemented + + # Since alphabetically "dev0" < "final" < "post1" < "post2", we don't + # have to do anything special with releaselevel for now. + return us < them diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/_version_info.pyi b/flask-app/venv/lib/python3.8/site-packages/attr/_version_info.pyi new file mode 100644 index 000000000..45ced0863 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/_version_info.pyi @@ -0,0 +1,9 @@ +class VersionInfo: + @property + def year(self) -> int: ... + @property + def minor(self) -> int: ... + @property + def micro(self) -> int: ... + @property + def releaselevel(self) -> str: ... diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/converters.py b/flask-app/venv/lib/python3.8/site-packages/attr/converters.py new file mode 100644 index 000000000..2777db6d0 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/converters.py @@ -0,0 +1,111 @@ +""" +Commonly useful converters. +""" + +from __future__ import absolute_import, division, print_function + +from ._compat import PY2 +from ._make import NOTHING, Factory, pipe + + +if not PY2: + import inspect + import typing + + +__all__ = [ + "pipe", + "optional", + "default_if_none", +] + + +def optional(converter): + """ + A converter that allows an attribute to be optional. An optional attribute + is one which can be set to ``None``. + + Type annotations will be inferred from the wrapped converter's, if it + has any. + + :param callable converter: the converter that is used for non-``None`` + values. + + .. versionadded:: 17.1.0 + """ + + def optional_converter(val): + if val is None: + return None + return converter(val) + + if not PY2: + sig = None + try: + sig = inspect.signature(converter) + except (ValueError, TypeError): # inspect failed + pass + if sig: + params = list(sig.parameters.values()) + if params and params[0].annotation is not inspect.Parameter.empty: + optional_converter.__annotations__["val"] = typing.Optional[ + params[0].annotation + ] + if sig.return_annotation is not inspect.Signature.empty: + optional_converter.__annotations__["return"] = typing.Optional[ + sig.return_annotation + ] + + return optional_converter + + +def default_if_none(default=NOTHING, factory=None): + """ + A converter that allows to replace ``None`` values by *default* or the + result of *factory*. + + :param default: Value to be used if ``None`` is passed. Passing an instance + of `attr.Factory` is supported, however the ``takes_self`` option + is *not*. + :param callable factory: A callable that takes no parameters whose result + is used if ``None`` is passed. + + :raises TypeError: If **neither** *default* or *factory* is passed. + :raises TypeError: If **both** *default* and *factory* are passed. + :raises ValueError: If an instance of `attr.Factory` is passed with + ``takes_self=True``. + + .. versionadded:: 18.2.0 + """ + if default is NOTHING and factory is None: + raise TypeError("Must pass either `default` or `factory`.") + + if default is not NOTHING and factory is not None: + raise TypeError( + "Must pass either `default` or `factory` but not both." + ) + + if factory is not None: + default = Factory(factory) + + if isinstance(default, Factory): + if default.takes_self: + raise ValueError( + "`takes_self` is not supported by default_if_none." + ) + + def default_if_none_converter(val): + if val is not None: + return val + + return default.factory() + + else: + + def default_if_none_converter(val): + if val is not None: + return val + + return default + + return default_if_none_converter diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/converters.pyi b/flask-app/venv/lib/python3.8/site-packages/attr/converters.pyi new file mode 100644 index 000000000..84a57590b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/converters.pyi @@ -0,0 +1,13 @@ +from typing import Callable, Optional, TypeVar, overload + +from . import _ConverterType + + +_T = TypeVar("_T") + +def pipe(*validators: _ConverterType) -> _ConverterType: ... +def optional(converter: _ConverterType) -> _ConverterType: ... +@overload +def default_if_none(default: _T) -> _ConverterType: ... +@overload +def default_if_none(*, factory: Callable[[], _T]) -> _ConverterType: ... diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/exceptions.py b/flask-app/venv/lib/python3.8/site-packages/attr/exceptions.py new file mode 100644 index 000000000..f6f9861be --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/exceptions.py @@ -0,0 +1,92 @@ +from __future__ import absolute_import, division, print_function + + +class FrozenError(AttributeError): + """ + A frozen/immutable instance or attribute have been attempted to be + modified. + + It mirrors the behavior of ``namedtuples`` by using the same error message + and subclassing `AttributeError`. + + .. versionadded:: 20.1.0 + """ + + msg = "can't set attribute" + args = [msg] + + +class FrozenInstanceError(FrozenError): + """ + A frozen instance has been attempted to be modified. + + .. versionadded:: 16.1.0 + """ + + +class FrozenAttributeError(FrozenError): + """ + A frozen attribute has been attempted to be modified. + + .. versionadded:: 20.1.0 + """ + + +class AttrsAttributeNotFoundError(ValueError): + """ + An ``attrs`` function couldn't find an attribute that the user asked for. + + .. versionadded:: 16.2.0 + """ + + +class NotAnAttrsClassError(ValueError): + """ + A non-``attrs`` class has been passed into an ``attrs`` function. + + .. versionadded:: 16.2.0 + """ + + +class DefaultAlreadySetError(RuntimeError): + """ + A default has been set using ``attr.ib()`` and is attempted to be reset + using the decorator. + + .. versionadded:: 17.1.0 + """ + + +class UnannotatedAttributeError(RuntimeError): + """ + A class with ``auto_attribs=True`` has an ``attr.ib()`` without a type + annotation. + + .. versionadded:: 17.3.0 + """ + + +class PythonTooOldError(RuntimeError): + """ + It was attempted to use an ``attrs`` feature that requires a newer Python + version. + + .. versionadded:: 18.2.0 + """ + + +class NotCallableError(TypeError): + """ + A ``attr.ib()`` requiring a callable has been set with a value + that is not callable. + + .. versionadded:: 19.2.0 + """ + + def __init__(self, msg, value): + super(TypeError, self).__init__(msg, value) + self.msg = msg + self.value = value + + def __str__(self): + return str(self.msg) diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/exceptions.pyi b/flask-app/venv/lib/python3.8/site-packages/attr/exceptions.pyi new file mode 100644 index 000000000..a800fb26b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/exceptions.pyi @@ -0,0 +1,18 @@ +from typing import Any + + +class FrozenError(AttributeError): + msg: str = ... + +class FrozenInstanceError(FrozenError): ... +class FrozenAttributeError(FrozenError): ... +class AttrsAttributeNotFoundError(ValueError): ... +class NotAnAttrsClassError(ValueError): ... +class DefaultAlreadySetError(RuntimeError): ... +class UnannotatedAttributeError(RuntimeError): ... +class PythonTooOldError(RuntimeError): ... + +class NotCallableError(TypeError): + msg: str = ... + value: Any = ... + def __init__(self, msg: str, value: Any) -> None: ... diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/filters.py b/flask-app/venv/lib/python3.8/site-packages/attr/filters.py new file mode 100644 index 000000000..dc47e8fa3 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/filters.py @@ -0,0 +1,52 @@ +""" +Commonly useful filters for `attr.asdict`. +""" + +from __future__ import absolute_import, division, print_function + +from ._compat import isclass +from ._make import Attribute + + +def _split_what(what): + """ + Returns a tuple of `frozenset`s of classes and attributes. + """ + return ( + frozenset(cls for cls in what if isclass(cls)), + frozenset(cls for cls in what if isinstance(cls, Attribute)), + ) + + +def include(*what): + """ + Whitelist *what*. + + :param what: What to whitelist. + :type what: `list` of `type` or `attr.Attribute`\\ s + + :rtype: `callable` + """ + cls, attrs = _split_what(what) + + def include_(attribute, value): + return value.__class__ in cls or attribute in attrs + + return include_ + + +def exclude(*what): + """ + Blacklist *what*. + + :param what: What to blacklist. + :type what: `list` of classes or `attr.Attribute`\\ s. + + :rtype: `callable` + """ + cls, attrs = _split_what(what) + + def exclude_(attribute, value): + return value.__class__ not in cls and attribute not in attrs + + return exclude_ diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/filters.pyi b/flask-app/venv/lib/python3.8/site-packages/attr/filters.pyi new file mode 100644 index 000000000..f7b63f1bb --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/filters.pyi @@ -0,0 +1,7 @@ +from typing import Any, Union + +from . import Attribute, _FilterType + + +def include(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... +def exclude(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/py.typed b/flask-app/venv/lib/python3.8/site-packages/attr/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/setters.py b/flask-app/venv/lib/python3.8/site-packages/attr/setters.py new file mode 100644 index 000000000..240014b3c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/setters.py @@ -0,0 +1,77 @@ +""" +Commonly used hooks for on_setattr. +""" + +from __future__ import absolute_import, division, print_function + +from . import _config +from .exceptions import FrozenAttributeError + + +def pipe(*setters): + """ + Run all *setters* and return the return value of the last one. + + .. versionadded:: 20.1.0 + """ + + def wrapped_pipe(instance, attrib, new_value): + rv = new_value + + for setter in setters: + rv = setter(instance, attrib, rv) + + return rv + + return wrapped_pipe + + +def frozen(_, __, ___): + """ + Prevent an attribute to be modified. + + .. versionadded:: 20.1.0 + """ + raise FrozenAttributeError() + + +def validate(instance, attrib, new_value): + """ + Run *attrib*'s validator on *new_value* if it has one. + + .. versionadded:: 20.1.0 + """ + if _config._run_validators is False: + return new_value + + v = attrib.validator + if not v: + return new_value + + v(instance, attrib, new_value) + + return new_value + + +def convert(instance, attrib, new_value): + """ + Run *attrib*'s converter -- if it has one -- on *new_value* and return the + result. + + .. versionadded:: 20.1.0 + """ + c = attrib.converter + if c: + return c(new_value) + + return new_value + + +NO_OP = object() +""" +Sentinel for disabling class-wide *on_setattr* hooks for certain attributes. + +Does not work in `pipe` or within lists. + +.. versionadded:: 20.1.0 +""" diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/setters.pyi b/flask-app/venv/lib/python3.8/site-packages/attr/setters.pyi new file mode 100644 index 000000000..a921e07de --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/setters.pyi @@ -0,0 +1,20 @@ +from typing import Any, NewType, NoReturn, TypeVar, cast + +from . import Attribute, _OnSetAttrType + + +_T = TypeVar("_T") + +def frozen( + instance: Any, attribute: Attribute[Any], new_value: Any +) -> NoReturn: ... +def pipe(*setters: _OnSetAttrType) -> _OnSetAttrType: ... +def validate(instance: Any, attribute: Attribute[_T], new_value: _T) -> _T: ... + +# convert is allowed to return Any, because they can be chained using pipe. +def convert( + instance: Any, attribute: Attribute[Any], new_value: Any +) -> Any: ... + +_NoOpType = NewType("_NoOpType", object) +NO_OP: _NoOpType diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/validators.py b/flask-app/venv/lib/python3.8/site-packages/attr/validators.py new file mode 100644 index 000000000..b9a73054e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/validators.py @@ -0,0 +1,379 @@ +""" +Commonly useful validators. +""" + +from __future__ import absolute_import, division, print_function + +import re + +from ._make import _AndValidator, and_, attrib, attrs +from .exceptions import NotCallableError + + +__all__ = [ + "and_", + "deep_iterable", + "deep_mapping", + "in_", + "instance_of", + "is_callable", + "matches_re", + "optional", + "provides", +] + + +@attrs(repr=False, slots=True, hash=True) +class _InstanceOfValidator(object): + type = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not isinstance(value, self.type): + raise TypeError( + "'{name}' must be {type!r} (got {value!r} that is a " + "{actual!r}).".format( + name=attr.name, + type=self.type, + actual=value.__class__, + value=value, + ), + attr, + self.type, + value, + ) + + def __repr__(self): + return "".format( + type=self.type + ) + + +def instance_of(type): + """ + A validator that raises a `TypeError` if the initializer is called + with a wrong type for this particular attribute (checks are performed using + `isinstance` therefore it's also valid to pass a tuple of types). + + :param type: The type to check for. + :type type: type or tuple of types + + :raises TypeError: With a human readable error message, the attribute + (of type `attr.Attribute`), the expected type, and the value it + got. + """ + return _InstanceOfValidator(type) + + +@attrs(repr=False, frozen=True, slots=True) +class _MatchesReValidator(object): + regex = attrib() + flags = attrib() + match_func = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.match_func(value): + raise ValueError( + "'{name}' must match regex {regex!r}" + " ({value!r} doesn't)".format( + name=attr.name, regex=self.regex.pattern, value=value + ), + attr, + self.regex, + value, + ) + + def __repr__(self): + return "".format( + regex=self.regex + ) + + +def matches_re(regex, flags=0, func=None): + r""" + A validator that raises `ValueError` if the initializer is called + with a string that doesn't match *regex*. + + :param str regex: a regex string to match against + :param int flags: flags that will be passed to the underlying re function + (default 0) + :param callable func: which underlying `re` function to call (options + are `re.fullmatch`, `re.search`, `re.match`, default + is ``None`` which means either `re.fullmatch` or an emulation of + it on Python 2). For performance reasons, they won't be used directly + but on a pre-`re.compile`\ ed pattern. + + .. versionadded:: 19.2.0 + """ + fullmatch = getattr(re, "fullmatch", None) + valid_funcs = (fullmatch, None, re.search, re.match) + if func not in valid_funcs: + raise ValueError( + "'func' must be one of %s." + % ( + ", ".join( + sorted( + e and e.__name__ or "None" for e in set(valid_funcs) + ) + ), + ) + ) + + pattern = re.compile(regex, flags) + if func is re.match: + match_func = pattern.match + elif func is re.search: + match_func = pattern.search + else: + if fullmatch: + match_func = pattern.fullmatch + else: + pattern = re.compile(r"(?:{})\Z".format(regex), flags) + match_func = pattern.match + + return _MatchesReValidator(pattern, flags, match_func) + + +@attrs(repr=False, slots=True, hash=True) +class _ProvidesValidator(object): + interface = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.interface.providedBy(value): + raise TypeError( + "'{name}' must provide {interface!r} which {value!r} " + "doesn't.".format( + name=attr.name, interface=self.interface, value=value + ), + attr, + self.interface, + value, + ) + + def __repr__(self): + return "".format( + interface=self.interface + ) + + +def provides(interface): + """ + A validator that raises a `TypeError` if the initializer is called + with an object that does not provide the requested *interface* (checks are + performed using ``interface.providedBy(value)`` (see `zope.interface + `_). + + :param interface: The interface to check for. + :type interface: ``zope.interface.Interface`` + + :raises TypeError: With a human readable error message, the attribute + (of type `attr.Attribute`), the expected interface, and the + value it got. + """ + return _ProvidesValidator(interface) + + +@attrs(repr=False, slots=True, hash=True) +class _OptionalValidator(object): + validator = attrib() + + def __call__(self, inst, attr, value): + if value is None: + return + + self.validator(inst, attr, value) + + def __repr__(self): + return "".format( + what=repr(self.validator) + ) + + +def optional(validator): + """ + A validator that makes an attribute optional. An optional attribute is one + which can be set to ``None`` in addition to satisfying the requirements of + the sub-validator. + + :param validator: A validator (or a list of validators) that is used for + non-``None`` values. + :type validator: callable or `list` of callables. + + .. versionadded:: 15.1.0 + .. versionchanged:: 17.1.0 *validator* can be a list of validators. + """ + if isinstance(validator, list): + return _OptionalValidator(_AndValidator(validator)) + return _OptionalValidator(validator) + + +@attrs(repr=False, slots=True, hash=True) +class _InValidator(object): + options = attrib() + + def __call__(self, inst, attr, value): + try: + in_options = value in self.options + except TypeError: # e.g. `1 in "abc"` + in_options = False + + if not in_options: + raise ValueError( + "'{name}' must be in {options!r} (got {value!r})".format( + name=attr.name, options=self.options, value=value + ) + ) + + def __repr__(self): + return "".format( + options=self.options + ) + + +def in_(options): + """ + A validator that raises a `ValueError` if the initializer is called + with a value that does not belong in the options provided. The check is + performed using ``value in options``. + + :param options: Allowed options. + :type options: list, tuple, `enum.Enum`, ... + + :raises ValueError: With a human readable error message, the attribute (of + type `attr.Attribute`), the expected options, and the value it + got. + + .. versionadded:: 17.1.0 + """ + return _InValidator(options) + + +@attrs(repr=False, slots=False, hash=True) +class _IsCallableValidator(object): + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not callable(value): + message = ( + "'{name}' must be callable " + "(got {value!r} that is a {actual!r})." + ) + raise NotCallableError( + msg=message.format( + name=attr.name, value=value, actual=value.__class__ + ), + value=value, + ) + + def __repr__(self): + return "" + + +def is_callable(): + """ + A validator that raises a `attr.exceptions.NotCallableError` if the + initializer is called with a value for this particular attribute + that is not callable. + + .. versionadded:: 19.1.0 + + :raises `attr.exceptions.NotCallableError`: With a human readable error + message containing the attribute (`attr.Attribute`) name, + and the value it got. + """ + return _IsCallableValidator() + + +@attrs(repr=False, slots=True, hash=True) +class _DeepIterable(object): + member_validator = attrib(validator=is_callable()) + iterable_validator = attrib( + default=None, validator=optional(is_callable()) + ) + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if self.iterable_validator is not None: + self.iterable_validator(inst, attr, value) + + for member in value: + self.member_validator(inst, attr, member) + + def __repr__(self): + iterable_identifier = ( + "" + if self.iterable_validator is None + else " {iterable!r}".format(iterable=self.iterable_validator) + ) + return ( + "" + ).format( + iterable_identifier=iterable_identifier, + member=self.member_validator, + ) + + +def deep_iterable(member_validator, iterable_validator=None): + """ + A validator that performs deep validation of an iterable. + + :param member_validator: Validator to apply to iterable members + :param iterable_validator: Validator to apply to iterable itself + (optional) + + .. versionadded:: 19.1.0 + + :raises TypeError: if any sub-validators fail + """ + return _DeepIterable(member_validator, iterable_validator) + + +@attrs(repr=False, slots=True, hash=True) +class _DeepMapping(object): + key_validator = attrib(validator=is_callable()) + value_validator = attrib(validator=is_callable()) + mapping_validator = attrib(default=None, validator=optional(is_callable())) + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if self.mapping_validator is not None: + self.mapping_validator(inst, attr, value) + + for key in value: + self.key_validator(inst, attr, key) + self.value_validator(inst, attr, value[key]) + + def __repr__(self): + return ( + "" + ).format(key=self.key_validator, value=self.value_validator) + + +def deep_mapping(key_validator, value_validator, mapping_validator=None): + """ + A validator that performs deep validation of a dictionary. + + :param key_validator: Validator to apply to dictionary keys + :param value_validator: Validator to apply to dictionary values + :param mapping_validator: Validator to apply to top-level mapping + attribute (optional) + + .. versionadded:: 19.1.0 + + :raises TypeError: if any sub-validators fail + """ + return _DeepMapping(key_validator, value_validator, mapping_validator) diff --git a/flask-app/venv/lib/python3.8/site-packages/attr/validators.pyi b/flask-app/venv/lib/python3.8/site-packages/attr/validators.pyi new file mode 100644 index 000000000..fe92aac42 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attr/validators.pyi @@ -0,0 +1,68 @@ +from typing import ( + Any, + AnyStr, + Callable, + Container, + Iterable, + List, + Mapping, + Match, + Optional, + Tuple, + Type, + TypeVar, + Union, + overload, +) + +from . import _ValidatorType + + +_T = TypeVar("_T") +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") +_T3 = TypeVar("_T3") +_I = TypeVar("_I", bound=Iterable) +_K = TypeVar("_K") +_V = TypeVar("_V") +_M = TypeVar("_M", bound=Mapping) + +# To be more precise on instance_of use some overloads. +# If there are more than 3 items in the tuple then we fall back to Any +@overload +def instance_of(type: Type[_T]) -> _ValidatorType[_T]: ... +@overload +def instance_of(type: Tuple[Type[_T]]) -> _ValidatorType[_T]: ... +@overload +def instance_of( + type: Tuple[Type[_T1], Type[_T2]] +) -> _ValidatorType[Union[_T1, _T2]]: ... +@overload +def instance_of( + type: Tuple[Type[_T1], Type[_T2], Type[_T3]] +) -> _ValidatorType[Union[_T1, _T2, _T3]]: ... +@overload +def instance_of(type: Tuple[type, ...]) -> _ValidatorType[Any]: ... +def provides(interface: Any) -> _ValidatorType[Any]: ... +def optional( + validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]] +) -> _ValidatorType[Optional[_T]]: ... +def in_(options: Container[_T]) -> _ValidatorType[_T]: ... +def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... +def matches_re( + regex: AnyStr, + flags: int = ..., + func: Optional[ + Callable[[AnyStr, AnyStr, int], Optional[Match[AnyStr]]] + ] = ..., +) -> _ValidatorType[AnyStr]: ... +def deep_iterable( + member_validator: _ValidatorType[_T], + iterable_validator: Optional[_ValidatorType[_I]] = ..., +) -> _ValidatorType[_I]: ... +def deep_mapping( + key_validator: _ValidatorType[_K], + value_validator: _ValidatorType[_V], + mapping_validator: Optional[_ValidatorType[_M]] = ..., +) -> _ValidatorType[_M]: ... +def is_callable() -> _ValidatorType[_T]: ... diff --git a/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/AUTHORS.rst b/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/AUTHORS.rst new file mode 100644 index 000000000..f14ef6c60 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/AUTHORS.rst @@ -0,0 +1,11 @@ +Credits +======= + +``attrs`` is written and maintained by `Hynek Schlawack `_. + +The development is kindly supported by `Variomedia AG `_. + +A full list of contributors can be found in `GitHub's overview `_. + +It’s the spiritual successor of `characteristic `_ and aspires to fix some of it clunkiness and unfortunate decisions. +Both were inspired by Twisted’s `FancyEqMixin `_ but both are implemented using class decorators because `subclassing is bad for you `_, m’kay? diff --git a/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/INSTALLER b/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/LICENSE b/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/LICENSE new file mode 100644 index 000000000..7ae3df930 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Hynek Schlawack + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/METADATA b/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/METADATA new file mode 100644 index 000000000..ceca5b9ad --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/METADATA @@ -0,0 +1,211 @@ +Metadata-Version: 2.1 +Name: attrs +Version: 21.2.0 +Summary: Classes Without Boilerplate +Home-page: https://www.attrs.org/ +Author: Hynek Schlawack +Author-email: hs@ox.cx +Maintainer: Hynek Schlawack +Maintainer-email: hs@ox.cx +License: MIT +Project-URL: Documentation, https://www.attrs.org/ +Project-URL: Changelog, https://www.attrs.org/en/stable/changelog.html +Project-URL: Bug Tracker, https://github.com/python-attrs/attrs/issues +Project-URL: Source Code, https://github.com/python-attrs/attrs +Project-URL: Funding, https://github.com/sponsors/hynek +Project-URL: Tidelift, https://tidelift.com/subscription/pkg/pypi-attrs?utm_source=pypi-attrs&utm_medium=pypi +Project-URL: Ko-fi, https://ko-fi.com/the_hynek +Keywords: class,attribute,boilerplate +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Natural Language :: English +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* +Description-Content-Type: text/x-rst +Provides-Extra: dev +Requires-Dist: coverage[toml] (>=5.0.2) ; extra == 'dev' +Requires-Dist: hypothesis ; extra == 'dev' +Requires-Dist: pympler ; extra == 'dev' +Requires-Dist: pytest (>=4.3.0) ; extra == 'dev' +Requires-Dist: six ; extra == 'dev' +Requires-Dist: mypy ; extra == 'dev' +Requires-Dist: pytest-mypy-plugins ; extra == 'dev' +Requires-Dist: zope.interface ; extra == 'dev' +Requires-Dist: furo ; extra == 'dev' +Requires-Dist: sphinx ; extra == 'dev' +Requires-Dist: sphinx-notfound-page ; extra == 'dev' +Requires-Dist: pre-commit ; extra == 'dev' +Provides-Extra: docs +Requires-Dist: furo ; extra == 'docs' +Requires-Dist: sphinx ; extra == 'docs' +Requires-Dist: zope.interface ; extra == 'docs' +Requires-Dist: sphinx-notfound-page ; extra == 'docs' +Provides-Extra: tests +Requires-Dist: coverage[toml] (>=5.0.2) ; extra == 'tests' +Requires-Dist: hypothesis ; extra == 'tests' +Requires-Dist: pympler ; extra == 'tests' +Requires-Dist: pytest (>=4.3.0) ; extra == 'tests' +Requires-Dist: six ; extra == 'tests' +Requires-Dist: mypy ; extra == 'tests' +Requires-Dist: pytest-mypy-plugins ; extra == 'tests' +Requires-Dist: zope.interface ; extra == 'tests' +Provides-Extra: tests_no_zope +Requires-Dist: coverage[toml] (>=5.0.2) ; extra == 'tests_no_zope' +Requires-Dist: hypothesis ; extra == 'tests_no_zope' +Requires-Dist: pympler ; extra == 'tests_no_zope' +Requires-Dist: pytest (>=4.3.0) ; extra == 'tests_no_zope' +Requires-Dist: six ; extra == 'tests_no_zope' +Requires-Dist: mypy ; extra == 'tests_no_zope' +Requires-Dist: pytest-mypy-plugins ; extra == 'tests_no_zope' + +====================================== +``attrs``: Classes Without Boilerplate +====================================== + + +``attrs`` is the Python package that will bring back the **joy** of **writing classes** by relieving you from the drudgery of implementing object protocols (aka `dunder `_ methods). +`Trusted by NASA `_ for Mars missions since 2020! + +Its main goal is to help you to write **concise** and **correct** software without slowing down your code. + +.. teaser-end + +For that, it gives you a class decorator and a way to declaratively define the attributes on that class: + +.. -code-begin- + +.. code-block:: pycon + + >>> import attr + + >>> @attr.s + ... class SomeClass(object): + ... a_number = attr.ib(default=42) + ... list_of_numbers = attr.ib(factory=list) + ... + ... def hard_math(self, another_number): + ... return self.a_number + sum(self.list_of_numbers) * another_number + + + >>> sc = SomeClass(1, [1, 2, 3]) + >>> sc + SomeClass(a_number=1, list_of_numbers=[1, 2, 3]) + + >>> sc.hard_math(3) + 19 + >>> sc == SomeClass(1, [1, 2, 3]) + True + >>> sc != SomeClass(2, [3, 2, 1]) + True + + >>> attr.asdict(sc) + {'a_number': 1, 'list_of_numbers': [1, 2, 3]} + + >>> SomeClass() + SomeClass(a_number=42, list_of_numbers=[]) + + >>> C = attr.make_class("C", ["a", "b"]) + >>> C("foo", "bar") + C(a='foo', b='bar') + + +After *declaring* your attributes ``attrs`` gives you: + +- a concise and explicit overview of the class's attributes, +- a nice human-readable ``__repr__``, +- a complete set of comparison methods (equality and ordering), +- an initializer, +- and much more, + +*without* writing dull boilerplate code again and again and *without* runtime performance penalties. + +On Python 3.6 and later, you can often even drop the calls to ``attr.ib()`` by using `type annotations `_. + +This gives you the power to use actual classes with actual types in your code instead of confusing ``tuple``\ s or `confusingly behaving `_ ``namedtuple``\ s. +Which in turn encourages you to write *small classes* that do `one thing well `_. +Never again violate the `single responsibility principle `_ just because implementing ``__init__`` et al is a painful drag. + + +.. -getting-help- + +Getting Help +============ + +Please use the ``python-attrs`` tag on `StackOverflow `_ to get help. + +Answering questions of your fellow developers is also a great way to help the project! + + +.. -project-information- + +Project Information +=================== + +``attrs`` is released under the `MIT `_ license, +its documentation lives at `Read the Docs `_, +the code on `GitHub `_, +and the latest release on `PyPI `_. +It’s rigorously tested on Python 2.7, 3.5+, and PyPy. + +We collect information on **third-party extensions** in our `wiki `_. +Feel free to browse and add your own! + +If you'd like to contribute to ``attrs`` you're most welcome and we've written `a little guide `_ to get you started! + + +``attrs`` for Enterprise +------------------------ + +Available as part of the Tidelift Subscription. + +The maintainers of ``attrs`` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. +Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. +`Learn more. `_ + + +Release Information +=================== + +21.2.0 (2021-05-07) +------------------- + +Backward-incompatible Changes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- We had to revert the recursive feature for ``attr.evolve()`` because it broke some use-cases -- sorry! + `#806 `_ +- Python 3.4 is now blocked using packaging metadata because ``attrs`` can't be imported on it anymore. + To ensure that 3.4 users can keep installing ``attrs`` easily, we will `yank `_ 21.1.0 from PyPI. + This has **no** consequences if you pin ``attrs`` to 21.1.0. + `#807 `_ + +`Full changelog `_. + +Credits +======= + +``attrs`` is written and maintained by `Hynek Schlawack `_. + +The development is kindly supported by `Variomedia AG `_. + +A full list of contributors can be found in `GitHub's overview `_. + +It’s the spiritual successor of `characteristic `_ and aspires to fix some of it clunkiness and unfortunate decisions. +Both were inspired by Twisted’s `FancyEqMixin `_ but both are implemented using class decorators because `subclassing is bad for you `_, m’kay? + + diff --git a/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/RECORD b/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/RECORD new file mode 100644 index 000000000..1f361a7b8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/RECORD @@ -0,0 +1,42 @@ +attr/__init__.py,sha256=OlF8DYZfrT1KFU6VKHkW4ia76ntgvvaqBbsNf8j6Woc,1613 +attr/__init__.pyi,sha256=bNx5qLa3MBtgaf9P5OP2uwuv0xbS7HUrEfLlkrAsrr8,14837 +attr/__pycache__/__init__.cpython-38.pyc,, +attr/__pycache__/_cmp.cpython-38.pyc,, +attr/__pycache__/_compat.cpython-38.pyc,, +attr/__pycache__/_config.cpython-38.pyc,, +attr/__pycache__/_funcs.cpython-38.pyc,, +attr/__pycache__/_make.cpython-38.pyc,, +attr/__pycache__/_next_gen.cpython-38.pyc,, +attr/__pycache__/_version_info.cpython-38.pyc,, +attr/__pycache__/converters.cpython-38.pyc,, +attr/__pycache__/exceptions.cpython-38.pyc,, +attr/__pycache__/filters.cpython-38.pyc,, +attr/__pycache__/setters.cpython-38.pyc,, +attr/__pycache__/validators.cpython-38.pyc,, +attr/_cmp.py,sha256=CB01fdAcVk9Uwho7qdhrpK1ss9lilIeKoY-WJ-EaZYA,4133 +attr/_cmp.pyi,sha256=APRWqmFwHtTrapyy-vNKovjF9dA-HPi-AqqidjgvLpQ,318 +attr/_compat.py,sha256=hYZsXQOKJzAIAPPEzo-Y4aF0DMjhCXEp-nr1gSxVVG4,7562 +attr/_config.py,sha256=_KvW0mQdH2PYjHc0YfIUaV_o2pVfM7ziMEYTxwmEhOA,514 +attr/_funcs.py,sha256=azJeF9YIMg3lP2qeQyuGhrrcJkfTjm7OLm2u4MhPTqs,13398 +attr/_make.py,sha256=xrK0rSAYDINJF-yGgb_Qb2DHuEaKRmrs102mkO0LI5c,97743 +attr/_next_gen.py,sha256=aZEIlr2XlPVzJ_SWSNRAEx07jgqbtHWQm3PnaOXTMyw,4072 +attr/_version_info.py,sha256=azMi1lNelb3cJvvYUMXsXVbUANkRzbD5IEiaXVpeVr4,2162 +attr/_version_info.pyi,sha256=x_M3L3WuB7r_ULXAWjx959udKQ4HLB8l-hsc1FDGNvk,209 +attr/converters.py,sha256=mn8pLVYzzl-WvmlNe52HM2ukSkuO4a12mrTaHpQjX9c,3039 +attr/converters.pyi,sha256=L7eN2rEXCNVOkh1hYP-GVbWtyO3e6eKOBvJR-hK_h1M,382 +attr/exceptions.py,sha256=6dC-9b6_nTG066z9sw0TP_Tx4vJaIC5RImMONTkDM6Q,1949 +attr/exceptions.pyi,sha256=Ydjpt9xbNLM8HUEhayegA3c0xIBc75kpRgtiv0qsLCs,540 +attr/filters.py,sha256=weDxwATsa69T_0bPVjiM1fGsciAMQmwhY5G8Jm5BxuI,1098 +attr/filters.pyi,sha256=jUFN1Nqx2x5ayyLLHzsW5hHObjd6RudZjnj-ENAJdWk,216 +attr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +attr/setters.py,sha256=0ElzHwdVK3dsYcQi2CXkFvhx8fNxUI5OVhw8SWeaKmA,1434 +attr/setters.pyi,sha256=kTxNSnrItMgRpFDyIwvtFd6xYtqnOWwi4UVks4dskRY,574 +attr/validators.py,sha256=6DBx1jt4oZxx1ppvx6JWqm9-UAsYpXC4HTwxJilCeRg,11497 +attr/validators.pyi,sha256=qN6dsUdWh2UkLaX46JJ86lzYlhy4sh8z66fTXgJQO60,1870 +attrs-21.2.0.dist-info/AUTHORS.rst,sha256=wsqCNbGz_mklcJrt54APIZHZpoTIJLkXqEhhn4Nd8hc,752 +attrs-21.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +attrs-21.2.0.dist-info/LICENSE,sha256=v2WaKLSSQGAvVrvfSQy-LsUJsVuY-Z17GaUsdA4yeGM,1082 +attrs-21.2.0.dist-info/METADATA,sha256=oaarWZ5r9x96ZwIcBvpmzpyt6ADyZP2QYjYVZrJrrEQ,9097 +attrs-21.2.0.dist-info/RECORD,, +attrs-21.2.0.dist-info/WHEEL,sha256=Z-nyYpwrcSqxfdux5Mbn_DQ525iP7J2DG3JgGvOYyTQ,110 +attrs-21.2.0.dist-info/top_level.txt,sha256=tlRYMddkRlKPqJ96wP2_j9uEsmcNHgD2SbuWd4CzGVU,5 diff --git a/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/WHEEL b/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/WHEEL new file mode 100644 index 000000000..01b8fc7d4 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.36.2) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/top_level.txt b/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/top_level.txt new file mode 100644 index 000000000..66a062d88 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/attrs-21.2.0.dist-info/top_level.txt @@ -0,0 +1 @@ +attr diff --git a/flask-app/venv/lib/python3.8/site-packages/benchmarks/__init__.py b/flask-app/venv/lib/python3.8/site-packages/benchmarks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/flask-app/venv/lib/python3.8/site-packages/benchmarks/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/benchmarks/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..c5aa9d167 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/benchmarks/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/benchmarks/bm/__init__.py b/flask-app/venv/lib/python3.8/site-packages/benchmarks/bm/__init__.py new file mode 100644 index 000000000..fb6b8b1e7 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/benchmarks/bm/__init__.py @@ -0,0 +1,8 @@ +from ._scenario import Scenario +from ._scenario import var + + +__all__ = [ + "var", + "Scenario", +] diff --git a/flask-app/venv/lib/python3.8/site-packages/benchmarks/bm/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/benchmarks/bm/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..cb150789b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/benchmarks/bm/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/benchmarks/bm/__pycache__/_scenario.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/benchmarks/bm/__pycache__/_scenario.cpython-38.pyc new file mode 100644 index 000000000..7b13badef Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/benchmarks/bm/__pycache__/_scenario.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/benchmarks/bm/_scenario.py b/flask-app/venv/lib/python3.8/site-packages/benchmarks/bm/_scenario.py new file mode 100644 index 000000000..4b95aa24a --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/benchmarks/bm/_scenario.py @@ -0,0 +1,77 @@ +import abc +import time + +import attr +import pyperf +import six + + +var = attr.ib + + +def _register(scenario_cls): + """Registers a scenario for benchmarking.""" + # This extends pyperf's runner by registering arguments for the scenario config + def add_cmdline_args(cmd, args): + for field in attr.fields(scenario_cls): + if hasattr(args, field.name): + cmd.extend(("--{}".format(field.name), str(getattr(args, field.name)))) + + runner = pyperf.Runner(add_cmdline_args=add_cmdline_args) + cmd = runner.argparser + + for field in attr.fields(scenario_cls): + cmd.add_argument("--{}".format(field.name), type=field.type) + + parsed_args = runner.parse_args() + + config_dict = { + field.name: getattr(parsed_args, field.name) + for field in attr.fields(scenario_cls) + if hasattr(parsed_args, field.name) + } + scenario = scenario_cls(**config_dict) + + runner.bench_time_func(scenario.scenario_name, scenario._pyperf) + + +class ScenarioMeta(abc.ABCMeta): + def __init__(cls, name, bases, _dict): + super(ScenarioMeta, cls).__init__(name, bases, _dict) + + # Make sure every sub-class is wrapped by `attr.s` + cls = attr.s()(cls) + + # Do not register the base Scenario class + # DEV: We cannot compare `cls` to `Scenario` since it doesn't exist yet + if cls.__module__ != __name__: + _register(cls) + + +class Scenario(six.with_metaclass(ScenarioMeta)): + """The base class for specifying a benchmark.""" + + name = attr.ib(type=str) + + @property + def scenario_name(self): + return "{}-{}".format(self.__class__.__name__.lower(), self.name) + + @abc.abstractmethod + def run(self): + """Returns a context manager that yields a function to be run for performance testing.""" + pass + + def _pyperf(self, loops): + rungen = self.run() + run = next(rungen) + t0 = time.perf_counter() + run(loops) + dt = time.perf_counter() - t0 + try: + # perform any teardown + next(rungen) + except StopIteration: + pass + finally: + return dt diff --git a/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/INSTALLER b/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/LICENSE.rst b/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/LICENSE.rst new file mode 100644 index 000000000..d12a84918 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/LICENSE.rst @@ -0,0 +1,28 @@ +Copyright 2014 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/METADATA b/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/METADATA new file mode 100644 index 000000000..2c8da01a5 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/METADATA @@ -0,0 +1,111 @@ +Metadata-Version: 2.1 +Name: click +Version: 8.0.3 +Summary: Composable command line interface toolkit +Home-page: https://palletsprojects.com/p/click/ +Author: Armin Ronacher +Author-email: armin.ronacher@active-4.com +Maintainer: Pallets +Maintainer-email: contact@palletsprojects.com +License: BSD-3-Clause +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Documentation, https://click.palletsprojects.com/ +Project-URL: Changes, https://click.palletsprojects.com/changes/ +Project-URL: Source Code, https://github.com/pallets/click/ +Project-URL: Issue Tracker, https://github.com/pallets/click/issues/ +Project-URL: Twitter, https://twitter.com/PalletsTeam +Project-URL: Chat, https://discord.gg/pallets +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst +License-File: LICENSE.rst +Requires-Dist: colorama ; platform_system == "Windows" +Requires-Dist: importlib-metadata ; python_version < "3.8" + +\$ click\_ +========== + +Click is a Python package for creating beautiful command line interfaces +in a composable way with as little code as necessary. It's the "Command +Line Interface Creation Kit". It's highly configurable but comes with +sensible defaults out of the box. + +It aims to make the process of writing command line tools quick and fun +while also preventing any frustration caused by the inability to +implement an intended CLI API. + +Click in three points: + +- Arbitrary nesting of commands +- Automatic help page generation +- Supports lazy loading of subcommands at runtime + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + $ pip install -U click + +.. _pip: https://pip.pypa.io/en/stable/getting-started/ + + +A Simple Example +---------------- + +.. code-block:: python + + import click + + @click.command() + @click.option("--count", default=1, help="Number of greetings.") + @click.option("--name", prompt="Your name", help="The person to greet.") + def hello(count, name): + """Simple program that greets NAME for a total of COUNT times.""" + for _ in range(count): + click.echo(f"Hello, {name}!") + + if __name__ == '__main__': + hello() + +.. code-block:: text + + $ python hello.py --count=3 + Your name: Click + Hello, Click! + Hello, Click! + Hello, Click! + + +Donate +------ + +The Pallets organization develops and supports Click and other popular +packages. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, `please +donate today`_. + +.. _please donate today: https://palletsprojects.com/donate + + +Links +----- + +- Documentation: https://click.palletsprojects.com/ +- Changes: https://click.palletsprojects.com/changes/ +- PyPI Releases: https://pypi.org/project/click/ +- Source Code: https://github.com/pallets/click +- Issue Tracker: https://github.com/pallets/click/issues +- Website: https://palletsprojects.com/p/click +- Twitter: https://twitter.com/PalletsTeam +- Chat: https://discord.gg/pallets + + diff --git a/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/RECORD b/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/RECORD new file mode 100644 index 000000000..263a6c699 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/RECORD @@ -0,0 +1,41 @@ +click-8.0.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +click-8.0.3.dist-info/LICENSE.rst,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475 +click-8.0.3.dist-info/METADATA,sha256=_0jCOf3DdGPvKUZUlBukeb1t6Pnxmm_OMGpaBoDthfc,3247 +click-8.0.3.dist-info/RECORD,, +click-8.0.3.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +click-8.0.3.dist-info/top_level.txt,sha256=J1ZQogalYS4pphY_lPECoNMfw0HzTSrZglC4Yfwo4xA,6 +click/__init__.py,sha256=YkIrDg7-0g5aBS6D2pDe58j3MOaFylHED2_8OXh2fnM,3243 +click/__pycache__/__init__.cpython-38.pyc,, +click/__pycache__/_compat.cpython-38.pyc,, +click/__pycache__/_termui_impl.cpython-38.pyc,, +click/__pycache__/_textwrap.cpython-38.pyc,, +click/__pycache__/_unicodefun.cpython-38.pyc,, +click/__pycache__/_winconsole.cpython-38.pyc,, +click/__pycache__/core.cpython-38.pyc,, +click/__pycache__/decorators.cpython-38.pyc,, +click/__pycache__/exceptions.cpython-38.pyc,, +click/__pycache__/formatting.cpython-38.pyc,, +click/__pycache__/globals.cpython-38.pyc,, +click/__pycache__/parser.cpython-38.pyc,, +click/__pycache__/shell_completion.cpython-38.pyc,, +click/__pycache__/termui.cpython-38.pyc,, +click/__pycache__/testing.cpython-38.pyc,, +click/__pycache__/types.cpython-38.pyc,, +click/__pycache__/utils.cpython-38.pyc,, +click/_compat.py,sha256=P15KQumAZC2F2MFe_JSRbvVOJcNosQfMDrdZq0ReCLM,18814 +click/_termui_impl.py,sha256=z78J5HF_RTsOBhjNLjoigaqRap3P2pWwEDDAjoZzgUg,23452 +click/_textwrap.py,sha256=10fQ64OcBUMuK7mFvh8363_uoOxPlRItZBmKzRJDgoY,1353 +click/_unicodefun.py,sha256=JKSh1oSwG_zbjAu4TBCa9tQde2P9FiYcf4MBfy5NdT8,3201 +click/_winconsole.py,sha256=5ju3jQkcZD0W27WEMGqmEP4y_crUVzPCqsX_FYb7BO0,7860 +click/core.py,sha256=k4PA2z0BT_dmed9I52Q2VLi8r6ekTMCtCQzw2y915Xs,111478 +click/decorators.py,sha256=sGkXJGmP7eLtjtmPl_Un2uBTlrhK8s2L22n-yBiDwTw,14864 +click/exceptions.py,sha256=7gDaLGuFZBeCNwY9ERMsF2-Z3R9Fvq09Zc6IZSKjseo,9167 +click/formatting.py,sha256=Frf0-5W33-loyY_i9qrwXR8-STnW3m5gvyxLVUdyxyk,9706 +click/globals.py,sha256=kGPzxq55Ug4dFUrgRV-5oHVPOPdLCUhmYolbrrVBo8g,1985 +click/parser.py,sha256=cAEt1uQR8gq3-S9ysqbVU-fdAZNvilxw4ReJ_T1OQMk,19044 +click/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +click/shell_completion.py,sha256=_hPI12T9Ex-y5a3WunWnlH0Gca2_urzXFXkDnt7G6Ow,18001 +click/termui.py,sha256=Rp2gFE8x7j8sEIoFMOcPmuqxJQVWWTrwEzyC14-sPAw,29006 +click/testing.py,sha256=kLR5Qcny1OlgxaGB3gweTr0gQe1yVlmgQRn2esA2Fz4,16020 +click/types.py,sha256=VoNZnIlRBAtRRgzavdqVnyfzY5y4U4qzVGI1UvvX1ls,35391 +click/utils.py,sha256=avYwX-3l2KkdJNUo8NmncZSoAdEmniQ_M5sdsSYloJ4,18759 diff --git a/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/WHEEL b/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/WHEEL new file mode 100644 index 000000000..5bad85fdc --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/top_level.txt b/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/top_level.txt new file mode 100644 index 000000000..dca9a9096 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click-8.0.3.dist-info/top_level.txt @@ -0,0 +1 @@ +click diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__init__.py b/flask-app/venv/lib/python3.8/site-packages/click/__init__.py new file mode 100644 index 000000000..a2ed5d135 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/__init__.py @@ -0,0 +1,75 @@ +""" +Click is a simple Python module inspired by the stdlib optparse to make +writing command line scripts fun. Unlike other modules, it's based +around a simple API that does not come with too much magic and is +composable. +""" +from .core import Argument as Argument +from .core import BaseCommand as BaseCommand +from .core import Command as Command +from .core import CommandCollection as CommandCollection +from .core import Context as Context +from .core import Group as Group +from .core import MultiCommand as MultiCommand +from .core import Option as Option +from .core import Parameter as Parameter +from .decorators import argument as argument +from .decorators import command as command +from .decorators import confirmation_option as confirmation_option +from .decorators import group as group +from .decorators import help_option as help_option +from .decorators import make_pass_decorator as make_pass_decorator +from .decorators import option as option +from .decorators import pass_context as pass_context +from .decorators import pass_obj as pass_obj +from .decorators import password_option as password_option +from .decorators import version_option as version_option +from .exceptions import Abort as Abort +from .exceptions import BadArgumentUsage as BadArgumentUsage +from .exceptions import BadOptionUsage as BadOptionUsage +from .exceptions import BadParameter as BadParameter +from .exceptions import ClickException as ClickException +from .exceptions import FileError as FileError +from .exceptions import MissingParameter as MissingParameter +from .exceptions import NoSuchOption as NoSuchOption +from .exceptions import UsageError as UsageError +from .formatting import HelpFormatter as HelpFormatter +from .formatting import wrap_text as wrap_text +from .globals import get_current_context as get_current_context +from .parser import OptionParser as OptionParser +from .termui import clear as clear +from .termui import confirm as confirm +from .termui import echo_via_pager as echo_via_pager +from .termui import edit as edit +from .termui import get_terminal_size as get_terminal_size +from .termui import getchar as getchar +from .termui import launch as launch +from .termui import pause as pause +from .termui import progressbar as progressbar +from .termui import prompt as prompt +from .termui import secho as secho +from .termui import style as style +from .termui import unstyle as unstyle +from .types import BOOL as BOOL +from .types import Choice as Choice +from .types import DateTime as DateTime +from .types import File as File +from .types import FLOAT as FLOAT +from .types import FloatRange as FloatRange +from .types import INT as INT +from .types import IntRange as IntRange +from .types import ParamType as ParamType +from .types import Path as Path +from .types import STRING as STRING +from .types import Tuple as Tuple +from .types import UNPROCESSED as UNPROCESSED +from .types import UUID as UUID +from .utils import echo as echo +from .utils import format_filename as format_filename +from .utils import get_app_dir as get_app_dir +from .utils import get_binary_stream as get_binary_stream +from .utils import get_os_args as get_os_args +from .utils import get_text_stream as get_text_stream +from .utils import open_file as open_file + +__version__ = "8.0.3" diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..13b1f377e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/_compat.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/_compat.cpython-38.pyc new file mode 100644 index 000000000..fcc2143bb Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/_compat.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/_termui_impl.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/_termui_impl.cpython-38.pyc new file mode 100644 index 000000000..dc3dbef0c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/_termui_impl.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/_textwrap.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/_textwrap.cpython-38.pyc new file mode 100644 index 000000000..ee866cfe1 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/_textwrap.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/_unicodefun.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/_unicodefun.cpython-38.pyc new file mode 100644 index 000000000..d389c0ceb Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/_unicodefun.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/_winconsole.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/_winconsole.cpython-38.pyc new file mode 100644 index 000000000..60e494396 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/_winconsole.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/core.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/core.cpython-38.pyc new file mode 100644 index 000000000..9fbf1b752 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/core.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/decorators.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/decorators.cpython-38.pyc new file mode 100644 index 000000000..71a0db9f3 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/decorators.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/exceptions.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/exceptions.cpython-38.pyc new file mode 100644 index 000000000..280bffb85 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/exceptions.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/formatting.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/formatting.cpython-38.pyc new file mode 100644 index 000000000..929650402 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/formatting.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/globals.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/globals.cpython-38.pyc new file mode 100644 index 000000000..efe45426c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/globals.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/parser.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/parser.cpython-38.pyc new file mode 100644 index 000000000..fbb3c113f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/parser.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/shell_completion.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/shell_completion.cpython-38.pyc new file mode 100644 index 000000000..64aae6010 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/shell_completion.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/termui.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/termui.cpython-38.pyc new file mode 100644 index 000000000..690ba4f95 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/termui.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/testing.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/testing.cpython-38.pyc new file mode 100644 index 000000000..3f77a61b5 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/testing.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/types.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/types.cpython-38.pyc new file mode 100644 index 000000000..e58a874b1 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/types.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/utils.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/utils.cpython-38.pyc new file mode 100644 index 000000000..5004c68fc Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/click/__pycache__/utils.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/click/_compat.py b/flask-app/venv/lib/python3.8/site-packages/click/_compat.py new file mode 100644 index 000000000..b9e1f0d39 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/_compat.py @@ -0,0 +1,627 @@ +import codecs +import io +import os +import re +import sys +import typing as t +from weakref import WeakKeyDictionary + +CYGWIN = sys.platform.startswith("cygwin") +MSYS2 = sys.platform.startswith("win") and ("GCC" in sys.version) +# Determine local App Engine environment, per Google's own suggestion +APP_ENGINE = "APPENGINE_RUNTIME" in os.environ and "Development/" in os.environ.get( + "SERVER_SOFTWARE", "" +) +WIN = sys.platform.startswith("win") and not APP_ENGINE and not MSYS2 +auto_wrap_for_ansi: t.Optional[t.Callable[[t.TextIO], t.TextIO]] = None +_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]") + + +def get_filesystem_encoding() -> str: + return sys.getfilesystemencoding() or sys.getdefaultencoding() + + +def _make_text_stream( + stream: t.BinaryIO, + encoding: t.Optional[str], + errors: t.Optional[str], + force_readable: bool = False, + force_writable: bool = False, +) -> t.TextIO: + if encoding is None: + encoding = get_best_encoding(stream) + if errors is None: + errors = "replace" + return _NonClosingTextIOWrapper( + stream, + encoding, + errors, + line_buffering=True, + force_readable=force_readable, + force_writable=force_writable, + ) + + +def is_ascii_encoding(encoding: str) -> bool: + """Checks if a given encoding is ascii.""" + try: + return codecs.lookup(encoding).name == "ascii" + except LookupError: + return False + + +def get_best_encoding(stream: t.IO) -> str: + """Returns the default stream encoding if not found.""" + rv = getattr(stream, "encoding", None) or sys.getdefaultencoding() + if is_ascii_encoding(rv): + return "utf-8" + return rv + + +class _NonClosingTextIOWrapper(io.TextIOWrapper): + def __init__( + self, + stream: t.BinaryIO, + encoding: t.Optional[str], + errors: t.Optional[str], + force_readable: bool = False, + force_writable: bool = False, + **extra: t.Any, + ) -> None: + self._stream = stream = t.cast( + t.BinaryIO, _FixupStream(stream, force_readable, force_writable) + ) + super().__init__(stream, encoding, errors, **extra) + + def __del__(self) -> None: + try: + self.detach() + except Exception: + pass + + def isatty(self) -> bool: + # https://bitbucket.org/pypy/pypy/issue/1803 + return self._stream.isatty() + + +class _FixupStream: + """The new io interface needs more from streams than streams + traditionally implement. As such, this fix-up code is necessary in + some circumstances. + + The forcing of readable and writable flags are there because some tools + put badly patched objects on sys (one such offender are certain version + of jupyter notebook). + """ + + def __init__( + self, + stream: t.BinaryIO, + force_readable: bool = False, + force_writable: bool = False, + ): + self._stream = stream + self._force_readable = force_readable + self._force_writable = force_writable + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._stream, name) + + def read1(self, size: int) -> bytes: + f = getattr(self._stream, "read1", None) + + if f is not None: + return t.cast(bytes, f(size)) + + return self._stream.read(size) + + def readable(self) -> bool: + if self._force_readable: + return True + x = getattr(self._stream, "readable", None) + if x is not None: + return t.cast(bool, x()) + try: + self._stream.read(0) + except Exception: + return False + return True + + def writable(self) -> bool: + if self._force_writable: + return True + x = getattr(self._stream, "writable", None) + if x is not None: + return t.cast(bool, x()) + try: + self._stream.write("") # type: ignore + except Exception: + try: + self._stream.write(b"") + except Exception: + return False + return True + + def seekable(self) -> bool: + x = getattr(self._stream, "seekable", None) + if x is not None: + return t.cast(bool, x()) + try: + self._stream.seek(self._stream.tell()) + except Exception: + return False + return True + + +def _is_binary_reader(stream: t.IO, default: bool = False) -> bool: + try: + return isinstance(stream.read(0), bytes) + except Exception: + return default + # This happens in some cases where the stream was already + # closed. In this case, we assume the default. + + +def _is_binary_writer(stream: t.IO, default: bool = False) -> bool: + try: + stream.write(b"") + except Exception: + try: + stream.write("") + return False + except Exception: + pass + return default + return True + + +def _find_binary_reader(stream: t.IO) -> t.Optional[t.BinaryIO]: + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detaching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_reader(stream, False): + return t.cast(t.BinaryIO, stream) + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_reader(buf, True): + return t.cast(t.BinaryIO, buf) + + return None + + +def _find_binary_writer(stream: t.IO) -> t.Optional[t.BinaryIO]: + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detaching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_writer(stream, False): + return t.cast(t.BinaryIO, stream) + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_writer(buf, True): + return t.cast(t.BinaryIO, buf) + + return None + + +def _stream_is_misconfigured(stream: t.TextIO) -> bool: + """A stream is misconfigured if its encoding is ASCII.""" + # If the stream does not have an encoding set, we assume it's set + # to ASCII. This appears to happen in certain unittest + # environments. It's not quite clear what the correct behavior is + # but this at least will force Click to recover somehow. + return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii") + + +def _is_compat_stream_attr(stream: t.TextIO, attr: str, value: t.Optional[str]) -> bool: + """A stream attribute is compatible if it is equal to the + desired value or the desired value is unset and the attribute + has a value. + """ + stream_value = getattr(stream, attr, None) + return stream_value == value or (value is None and stream_value is not None) + + +def _is_compatible_text_stream( + stream: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str] +) -> bool: + """Check if a stream's encoding and errors attributes are + compatible with the desired values. + """ + return _is_compat_stream_attr( + stream, "encoding", encoding + ) and _is_compat_stream_attr(stream, "errors", errors) + + +def _force_correct_text_stream( + text_stream: t.IO, + encoding: t.Optional[str], + errors: t.Optional[str], + is_binary: t.Callable[[t.IO, bool], bool], + find_binary: t.Callable[[t.IO], t.Optional[t.BinaryIO]], + force_readable: bool = False, + force_writable: bool = False, +) -> t.TextIO: + if is_binary(text_stream, False): + binary_reader = t.cast(t.BinaryIO, text_stream) + else: + text_stream = t.cast(t.TextIO, text_stream) + # If the stream looks compatible, and won't default to a + # misconfigured ascii encoding, return it as-is. + if _is_compatible_text_stream(text_stream, encoding, errors) and not ( + encoding is None and _stream_is_misconfigured(text_stream) + ): + return text_stream + + # Otherwise, get the underlying binary reader. + possible_binary_reader = find_binary(text_stream) + + # If that's not possible, silently use the original reader + # and get mojibake instead of exceptions. + if possible_binary_reader is None: + return text_stream + + binary_reader = possible_binary_reader + + # Default errors to replace instead of strict in order to get + # something that works. + if errors is None: + errors = "replace" + + # Wrap the binary stream in a text stream with the correct + # encoding parameters. + return _make_text_stream( + binary_reader, + encoding, + errors, + force_readable=force_readable, + force_writable=force_writable, + ) + + +def _force_correct_text_reader( + text_reader: t.IO, + encoding: t.Optional[str], + errors: t.Optional[str], + force_readable: bool = False, +) -> t.TextIO: + return _force_correct_text_stream( + text_reader, + encoding, + errors, + _is_binary_reader, + _find_binary_reader, + force_readable=force_readable, + ) + + +def _force_correct_text_writer( + text_writer: t.IO, + encoding: t.Optional[str], + errors: t.Optional[str], + force_writable: bool = False, +) -> t.TextIO: + return _force_correct_text_stream( + text_writer, + encoding, + errors, + _is_binary_writer, + _find_binary_writer, + force_writable=force_writable, + ) + + +def get_binary_stdin() -> t.BinaryIO: + reader = _find_binary_reader(sys.stdin) + if reader is None: + raise RuntimeError("Was not able to determine binary stream for sys.stdin.") + return reader + + +def get_binary_stdout() -> t.BinaryIO: + writer = _find_binary_writer(sys.stdout) + if writer is None: + raise RuntimeError("Was not able to determine binary stream for sys.stdout.") + return writer + + +def get_binary_stderr() -> t.BinaryIO: + writer = _find_binary_writer(sys.stderr) + if writer is None: + raise RuntimeError("Was not able to determine binary stream for sys.stderr.") + return writer + + +def get_text_stdin( + encoding: t.Optional[str] = None, errors: t.Optional[str] = None +) -> t.TextIO: + rv = _get_windows_console_stream(sys.stdin, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_reader(sys.stdin, encoding, errors, force_readable=True) + + +def get_text_stdout( + encoding: t.Optional[str] = None, errors: t.Optional[str] = None +) -> t.TextIO: + rv = _get_windows_console_stream(sys.stdout, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer(sys.stdout, encoding, errors, force_writable=True) + + +def get_text_stderr( + encoding: t.Optional[str] = None, errors: t.Optional[str] = None +) -> t.TextIO: + rv = _get_windows_console_stream(sys.stderr, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer(sys.stderr, encoding, errors, force_writable=True) + + +def _wrap_io_open( + file: t.Union[str, os.PathLike, int], + mode: str, + encoding: t.Optional[str], + errors: t.Optional[str], +) -> t.IO: + """Handles not passing ``encoding`` and ``errors`` in binary mode.""" + if "b" in mode: + return open(file, mode) + + return open(file, mode, encoding=encoding, errors=errors) + + +def open_stream( + filename: str, + mode: str = "r", + encoding: t.Optional[str] = None, + errors: t.Optional[str] = "strict", + atomic: bool = False, +) -> t.Tuple[t.IO, bool]: + binary = "b" in mode + + # Standard streams first. These are simple because they don't need + # special handling for the atomic flag. It's entirely ignored. + if filename == "-": + if any(m in mode for m in ["w", "a", "x"]): + if binary: + return get_binary_stdout(), False + return get_text_stdout(encoding=encoding, errors=errors), False + if binary: + return get_binary_stdin(), False + return get_text_stdin(encoding=encoding, errors=errors), False + + # Non-atomic writes directly go out through the regular open functions. + if not atomic: + return _wrap_io_open(filename, mode, encoding, errors), True + + # Some usability stuff for atomic writes + if "a" in mode: + raise ValueError( + "Appending to an existing file is not supported, because that" + " would involve an expensive `copy`-operation to a temporary" + " file. Open the file in normal `w`-mode and copy explicitly" + " if that's what you're after." + ) + if "x" in mode: + raise ValueError("Use the `overwrite`-parameter instead.") + if "w" not in mode: + raise ValueError("Atomic writes only make sense with `w`-mode.") + + # Atomic writes are more complicated. They work by opening a file + # as a proxy in the same folder and then using the fdopen + # functionality to wrap it in a Python file. Then we wrap it in an + # atomic file that moves the file over on close. + import errno + import random + + try: + perm: t.Optional[int] = os.stat(filename).st_mode + except OSError: + perm = None + + flags = os.O_RDWR | os.O_CREAT | os.O_EXCL + + if binary: + flags |= getattr(os, "O_BINARY", 0) + + while True: + tmp_filename = os.path.join( + os.path.dirname(filename), + f".__atomic-write{random.randrange(1 << 32):08x}", + ) + try: + fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm) + break + except OSError as e: + if e.errno == errno.EEXIST or ( + os.name == "nt" + and e.errno == errno.EACCES + and os.path.isdir(e.filename) + and os.access(e.filename, os.W_OK) + ): + continue + raise + + if perm is not None: + os.chmod(tmp_filename, perm) # in case perm includes bits in umask + + f = _wrap_io_open(fd, mode, encoding, errors) + af = _AtomicFile(f, tmp_filename, os.path.realpath(filename)) + return t.cast(t.IO, af), True + + +class _AtomicFile: + def __init__(self, f: t.IO, tmp_filename: str, real_filename: str) -> None: + self._f = f + self._tmp_filename = tmp_filename + self._real_filename = real_filename + self.closed = False + + @property + def name(self) -> str: + return self._real_filename + + def close(self, delete: bool = False) -> None: + if self.closed: + return + self._f.close() + os.replace(self._tmp_filename, self._real_filename) + self.closed = True + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._f, name) + + def __enter__(self) -> "_AtomicFile": + return self + + def __exit__(self, exc_type, exc_value, tb): # type: ignore + self.close(delete=exc_type is not None) + + def __repr__(self) -> str: + return repr(self._f) + + +def strip_ansi(value: str) -> str: + return _ansi_re.sub("", value) + + +def _is_jupyter_kernel_output(stream: t.IO) -> bool: + while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)): + stream = stream._stream + + return stream.__class__.__module__.startswith("ipykernel.") + + +def should_strip_ansi( + stream: t.Optional[t.IO] = None, color: t.Optional[bool] = None +) -> bool: + if color is None: + if stream is None: + stream = sys.stdin + return not isatty(stream) and not _is_jupyter_kernel_output(stream) + return not color + + +# On Windows, wrap the output streams with colorama to support ANSI +# color codes. +# NOTE: double check is needed so mypy does not analyze this on Linux +if sys.platform.startswith("win") and WIN: + from ._winconsole import _get_windows_console_stream + + def _get_argv_encoding() -> str: + import locale + + return locale.getpreferredencoding() + + _ansi_stream_wrappers: t.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary() + + def auto_wrap_for_ansi( + stream: t.TextIO, color: t.Optional[bool] = None + ) -> t.TextIO: + """Support ANSI color and style codes on Windows by wrapping a + stream with colorama. + """ + try: + cached = _ansi_stream_wrappers.get(stream) + except Exception: + cached = None + + if cached is not None: + return cached + + import colorama + + strip = should_strip_ansi(stream, color) + ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip) + rv = t.cast(t.TextIO, ansi_wrapper.stream) + _write = rv.write + + def _safe_write(s): + try: + return _write(s) + except BaseException: + ansi_wrapper.reset_all() + raise + + rv.write = _safe_write + + try: + _ansi_stream_wrappers[stream] = rv + except Exception: + pass + + return rv + + +else: + + def _get_argv_encoding() -> str: + return getattr(sys.stdin, "encoding", None) or get_filesystem_encoding() + + def _get_windows_console_stream( + f: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str] + ) -> t.Optional[t.TextIO]: + return None + + +def term_len(x: str) -> int: + return len(strip_ansi(x)) + + +def isatty(stream: t.IO) -> bool: + try: + return stream.isatty() + except Exception: + return False + + +def _make_cached_stream_func( + src_func: t.Callable[[], t.TextIO], wrapper_func: t.Callable[[], t.TextIO] +) -> t.Callable[[], t.TextIO]: + cache: t.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary() + + def func() -> t.TextIO: + stream = src_func() + try: + rv = cache.get(stream) + except Exception: + rv = None + if rv is not None: + return rv + rv = wrapper_func() + try: + cache[stream] = rv + except Exception: + pass + return rv + + return func + + +_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin) +_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout) +_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr) + + +binary_streams: t.Mapping[str, t.Callable[[], t.BinaryIO]] = { + "stdin": get_binary_stdin, + "stdout": get_binary_stdout, + "stderr": get_binary_stderr, +} + +text_streams: t.Mapping[ + str, t.Callable[[t.Optional[str], t.Optional[str]], t.TextIO] +] = { + "stdin": get_text_stdin, + "stdout": get_text_stdout, + "stderr": get_text_stderr, +} diff --git a/flask-app/venv/lib/python3.8/site-packages/click/_termui_impl.py b/flask-app/venv/lib/python3.8/site-packages/click/_termui_impl.py new file mode 100644 index 000000000..39c1d08f4 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/_termui_impl.py @@ -0,0 +1,718 @@ +""" +This module contains implementations for the termui module. To keep the +import time of Click down, some infrequently used functionality is +placed in this module and only imported as needed. +""" +import contextlib +import math +import os +import sys +import time +import typing as t +from gettext import gettext as _ + +from ._compat import _default_text_stdout +from ._compat import CYGWIN +from ._compat import get_best_encoding +from ._compat import isatty +from ._compat import open_stream +from ._compat import strip_ansi +from ._compat import term_len +from ._compat import WIN +from .exceptions import ClickException +from .utils import echo + +V = t.TypeVar("V") + +if os.name == "nt": + BEFORE_BAR = "\r" + AFTER_BAR = "\n" +else: + BEFORE_BAR = "\r\033[?25l" + AFTER_BAR = "\033[?25h\n" + + +class ProgressBar(t.Generic[V]): + def __init__( + self, + iterable: t.Optional[t.Iterable[V]], + length: t.Optional[int] = None, + fill_char: str = "#", + empty_char: str = " ", + bar_template: str = "%(bar)s", + info_sep: str = " ", + show_eta: bool = True, + show_percent: t.Optional[bool] = None, + show_pos: bool = False, + item_show_func: t.Optional[t.Callable[[t.Optional[V]], t.Optional[str]]] = None, + label: t.Optional[str] = None, + file: t.Optional[t.TextIO] = None, + color: t.Optional[bool] = None, + update_min_steps: int = 1, + width: int = 30, + ) -> None: + self.fill_char = fill_char + self.empty_char = empty_char + self.bar_template = bar_template + self.info_sep = info_sep + self.show_eta = show_eta + self.show_percent = show_percent + self.show_pos = show_pos + self.item_show_func = item_show_func + self.label = label or "" + if file is None: + file = _default_text_stdout() + self.file = file + self.color = color + self.update_min_steps = update_min_steps + self._completed_intervals = 0 + self.width = width + self.autowidth = width == 0 + + if length is None: + from operator import length_hint + + length = length_hint(iterable, -1) + + if length == -1: + length = None + if iterable is None: + if length is None: + raise TypeError("iterable or length is required") + iterable = t.cast(t.Iterable[V], range(length)) + self.iter = iter(iterable) + self.length = length + self.pos = 0 + self.avg: t.List[float] = [] + self.start = self.last_eta = time.time() + self.eta_known = False + self.finished = False + self.max_width: t.Optional[int] = None + self.entered = False + self.current_item: t.Optional[V] = None + self.is_hidden = not isatty(self.file) + self._last_line: t.Optional[str] = None + + def __enter__(self) -> "ProgressBar": + self.entered = True + self.render_progress() + return self + + def __exit__(self, exc_type, exc_value, tb): # type: ignore + self.render_finish() + + def __iter__(self) -> t.Iterator[V]: + if not self.entered: + raise RuntimeError("You need to use progress bars in a with block.") + self.render_progress() + return self.generator() + + def __next__(self) -> V: + # Iteration is defined in terms of a generator function, + # returned by iter(self); use that to define next(). This works + # because `self.iter` is an iterable consumed by that generator, + # so it is re-entry safe. Calling `next(self.generator())` + # twice works and does "what you want". + return next(iter(self)) + + def render_finish(self) -> None: + if self.is_hidden: + return + self.file.write(AFTER_BAR) + self.file.flush() + + @property + def pct(self) -> float: + if self.finished: + return 1.0 + return min(self.pos / (float(self.length or 1) or 1), 1.0) + + @property + def time_per_iteration(self) -> float: + if not self.avg: + return 0.0 + return sum(self.avg) / float(len(self.avg)) + + @property + def eta(self) -> float: + if self.length is not None and not self.finished: + return self.time_per_iteration * (self.length - self.pos) + return 0.0 + + def format_eta(self) -> str: + if self.eta_known: + t = int(self.eta) + seconds = t % 60 + t //= 60 + minutes = t % 60 + t //= 60 + hours = t % 24 + t //= 24 + if t > 0: + return f"{t}d {hours:02}:{minutes:02}:{seconds:02}" + else: + return f"{hours:02}:{minutes:02}:{seconds:02}" + return "" + + def format_pos(self) -> str: + pos = str(self.pos) + if self.length is not None: + pos += f"/{self.length}" + return pos + + def format_pct(self) -> str: + return f"{int(self.pct * 100): 4}%"[1:] + + def format_bar(self) -> str: + if self.length is not None: + bar_length = int(self.pct * self.width) + bar = self.fill_char * bar_length + bar += self.empty_char * (self.width - bar_length) + elif self.finished: + bar = self.fill_char * self.width + else: + chars = list(self.empty_char * (self.width or 1)) + if self.time_per_iteration != 0: + chars[ + int( + (math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5) + * self.width + ) + ] = self.fill_char + bar = "".join(chars) + return bar + + def format_progress_line(self) -> str: + show_percent = self.show_percent + + info_bits = [] + if self.length is not None and show_percent is None: + show_percent = not self.show_pos + + if self.show_pos: + info_bits.append(self.format_pos()) + if show_percent: + info_bits.append(self.format_pct()) + if self.show_eta and self.eta_known and not self.finished: + info_bits.append(self.format_eta()) + if self.item_show_func is not None: + item_info = self.item_show_func(self.current_item) + if item_info is not None: + info_bits.append(item_info) + + return ( + self.bar_template + % { + "label": self.label, + "bar": self.format_bar(), + "info": self.info_sep.join(info_bits), + } + ).rstrip() + + def render_progress(self) -> None: + import shutil + + if self.is_hidden: + # Only output the label as it changes if the output is not a + # TTY. Use file=stderr if you expect to be piping stdout. + if self._last_line != self.label: + self._last_line = self.label + echo(self.label, file=self.file, color=self.color) + + return + + buf = [] + # Update width in case the terminal has been resized + if self.autowidth: + old_width = self.width + self.width = 0 + clutter_length = term_len(self.format_progress_line()) + new_width = max(0, shutil.get_terminal_size().columns - clutter_length) + if new_width < old_width: + buf.append(BEFORE_BAR) + buf.append(" " * self.max_width) # type: ignore + self.max_width = new_width + self.width = new_width + + clear_width = self.width + if self.max_width is not None: + clear_width = self.max_width + + buf.append(BEFORE_BAR) + line = self.format_progress_line() + line_len = term_len(line) + if self.max_width is None or self.max_width < line_len: + self.max_width = line_len + + buf.append(line) + buf.append(" " * (clear_width - line_len)) + line = "".join(buf) + # Render the line only if it changed. + + if line != self._last_line: + self._last_line = line + echo(line, file=self.file, color=self.color, nl=False) + self.file.flush() + + def make_step(self, n_steps: int) -> None: + self.pos += n_steps + if self.length is not None and self.pos >= self.length: + self.finished = True + + if (time.time() - self.last_eta) < 1.0: + return + + self.last_eta = time.time() + + # self.avg is a rolling list of length <= 7 of steps where steps are + # defined as time elapsed divided by the total progress through + # self.length. + if self.pos: + step = (time.time() - self.start) / self.pos + else: + step = time.time() - self.start + + self.avg = self.avg[-6:] + [step] + + self.eta_known = self.length is not None + + def update(self, n_steps: int, current_item: t.Optional[V] = None) -> None: + """Update the progress bar by advancing a specified number of + steps, and optionally set the ``current_item`` for this new + position. + + :param n_steps: Number of steps to advance. + :param current_item: Optional item to set as ``current_item`` + for the updated position. + + .. versionchanged:: 8.0 + Added the ``current_item`` optional parameter. + + .. versionchanged:: 8.0 + Only render when the number of steps meets the + ``update_min_steps`` threshold. + """ + if current_item is not None: + self.current_item = current_item + + self._completed_intervals += n_steps + + if self._completed_intervals >= self.update_min_steps: + self.make_step(self._completed_intervals) + self.render_progress() + self._completed_intervals = 0 + + def finish(self) -> None: + self.eta_known = False + self.current_item = None + self.finished = True + + def generator(self) -> t.Iterator[V]: + """Return a generator which yields the items added to the bar + during construction, and updates the progress bar *after* the + yielded block returns. + """ + # WARNING: the iterator interface for `ProgressBar` relies on + # this and only works because this is a simple generator which + # doesn't create or manage additional state. If this function + # changes, the impact should be evaluated both against + # `iter(bar)` and `next(bar)`. `next()` in particular may call + # `self.generator()` repeatedly, and this must remain safe in + # order for that interface to work. + if not self.entered: + raise RuntimeError("You need to use progress bars in a with block.") + + if self.is_hidden: + yield from self.iter + else: + for rv in self.iter: + self.current_item = rv + + # This allows show_item_func to be updated before the + # item is processed. Only trigger at the beginning of + # the update interval. + if self._completed_intervals == 0: + self.render_progress() + + yield rv + self.update(1) + + self.finish() + self.render_progress() + + +def pager(generator: t.Iterable[str], color: t.Optional[bool] = None) -> None: + """Decide what method to use for paging through text.""" + stdout = _default_text_stdout() + if not isatty(sys.stdin) or not isatty(stdout): + return _nullpager(stdout, generator, color) + pager_cmd = (os.environ.get("PAGER", None) or "").strip() + if pager_cmd: + if WIN: + return _tempfilepager(generator, pager_cmd, color) + return _pipepager(generator, pager_cmd, color) + if os.environ.get("TERM") in ("dumb", "emacs"): + return _nullpager(stdout, generator, color) + if WIN or sys.platform.startswith("os2"): + return _tempfilepager(generator, "more <", color) + if hasattr(os, "system") and os.system("(less) 2>/dev/null") == 0: + return _pipepager(generator, "less", color) + + import tempfile + + fd, filename = tempfile.mkstemp() + os.close(fd) + try: + if hasattr(os, "system") and os.system(f'more "{filename}"') == 0: + return _pipepager(generator, "more", color) + return _nullpager(stdout, generator, color) + finally: + os.unlink(filename) + + +def _pipepager(generator: t.Iterable[str], cmd: str, color: t.Optional[bool]) -> None: + """Page through text by feeding it to another program. Invoking a + pager through this might support colors. + """ + import subprocess + + env = dict(os.environ) + + # If we're piping to less we might support colors under the + # condition that + cmd_detail = cmd.rsplit("/", 1)[-1].split() + if color is None and cmd_detail[0] == "less": + less_flags = f"{os.environ.get('LESS', '')}{' '.join(cmd_detail[1:])}" + if not less_flags: + env["LESS"] = "-R" + color = True + elif "r" in less_flags or "R" in less_flags: + color = True + + c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env) + stdin = t.cast(t.BinaryIO, c.stdin) + encoding = get_best_encoding(stdin) + try: + for text in generator: + if not color: + text = strip_ansi(text) + + stdin.write(text.encode(encoding, "replace")) + except (OSError, KeyboardInterrupt): + pass + else: + stdin.close() + + # Less doesn't respect ^C, but catches it for its own UI purposes (aborting + # search or other commands inside less). + # + # That means when the user hits ^C, the parent process (click) terminates, + # but less is still alive, paging the output and messing up the terminal. + # + # If the user wants to make the pager exit on ^C, they should set + # `LESS='-K'`. It's not our decision to make. + while True: + try: + c.wait() + except KeyboardInterrupt: + pass + else: + break + + +def _tempfilepager( + generator: t.Iterable[str], cmd: str, color: t.Optional[bool] +) -> None: + """Page through text by invoking a program on a temporary file.""" + import tempfile + + fd, filename = tempfile.mkstemp() + # TODO: This never terminates if the passed generator never terminates. + text = "".join(generator) + if not color: + text = strip_ansi(text) + encoding = get_best_encoding(sys.stdout) + with open_stream(filename, "wb")[0] as f: + f.write(text.encode(encoding)) + try: + os.system(f'{cmd} "{filename}"') + finally: + os.close(fd) + os.unlink(filename) + + +def _nullpager( + stream: t.TextIO, generator: t.Iterable[str], color: t.Optional[bool] +) -> None: + """Simply print unformatted text. This is the ultimate fallback.""" + for text in generator: + if not color: + text = strip_ansi(text) + stream.write(text) + + +class Editor: + def __init__( + self, + editor: t.Optional[str] = None, + env: t.Optional[t.Mapping[str, str]] = None, + require_save: bool = True, + extension: str = ".txt", + ) -> None: + self.editor = editor + self.env = env + self.require_save = require_save + self.extension = extension + + def get_editor(self) -> str: + if self.editor is not None: + return self.editor + for key in "VISUAL", "EDITOR": + rv = os.environ.get(key) + if rv: + return rv + if WIN: + return "notepad" + for editor in "sensible-editor", "vim", "nano": + if os.system(f"which {editor} >/dev/null 2>&1") == 0: + return editor + return "vi" + + def edit_file(self, filename: str) -> None: + import subprocess + + editor = self.get_editor() + environ: t.Optional[t.Dict[str, str]] = None + + if self.env: + environ = os.environ.copy() + environ.update(self.env) + + try: + c = subprocess.Popen(f'{editor} "{filename}"', env=environ, shell=True) + exit_code = c.wait() + if exit_code != 0: + raise ClickException( + _("{editor}: Editing failed").format(editor=editor) + ) + except OSError as e: + raise ClickException( + _("{editor}: Editing failed: {e}").format(editor=editor, e=e) + ) from e + + def edit(self, text: t.Optional[t.AnyStr]) -> t.Optional[t.AnyStr]: + import tempfile + + if not text: + data = b"" + elif isinstance(text, (bytes, bytearray)): + data = text + else: + if text and not text.endswith("\n"): + text += "\n" + + if WIN: + data = text.replace("\n", "\r\n").encode("utf-8-sig") + else: + data = text.encode("utf-8") + + fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension) + f: t.BinaryIO + + try: + with os.fdopen(fd, "wb") as f: + f.write(data) + + # If the filesystem resolution is 1 second, like Mac OS + # 10.12 Extended, or 2 seconds, like FAT32, and the editor + # closes very fast, require_save can fail. Set the modified + # time to be 2 seconds in the past to work around this. + os.utime(name, (os.path.getatime(name), os.path.getmtime(name) - 2)) + # Depending on the resolution, the exact value might not be + # recorded, so get the new recorded value. + timestamp = os.path.getmtime(name) + + self.edit_file(name) + + if self.require_save and os.path.getmtime(name) == timestamp: + return None + + with open(name, "rb") as f: + rv = f.read() + + if isinstance(text, (bytes, bytearray)): + return rv + + return rv.decode("utf-8-sig").replace("\r\n", "\n") # type: ignore + finally: + os.unlink(name) + + +def open_url(url: str, wait: bool = False, locate: bool = False) -> int: + import subprocess + + def _unquote_file(url: str) -> str: + from urllib.parse import unquote + + if url.startswith("file://"): + url = unquote(url[7:]) + + return url + + if sys.platform == "darwin": + args = ["open"] + if wait: + args.append("-W") + if locate: + args.append("-R") + args.append(_unquote_file(url)) + null = open("/dev/null", "w") + try: + return subprocess.Popen(args, stderr=null).wait() + finally: + null.close() + elif WIN: + if locate: + url = _unquote_file(url.replace('"', "")) + args = f'explorer /select,"{url}"' + else: + url = url.replace('"', "") + wait_str = "/WAIT" if wait else "" + args = f'start {wait_str} "" "{url}"' + return os.system(args) + elif CYGWIN: + if locate: + url = os.path.dirname(_unquote_file(url).replace('"', "")) + args = f'cygstart "{url}"' + else: + url = url.replace('"', "") + wait_str = "-w" if wait else "" + args = f'cygstart {wait_str} "{url}"' + return os.system(args) + + try: + if locate: + url = os.path.dirname(_unquote_file(url)) or "." + else: + url = _unquote_file(url) + c = subprocess.Popen(["xdg-open", url]) + if wait: + return c.wait() + return 0 + except OSError: + if url.startswith(("http://", "https://")) and not locate and not wait: + import webbrowser + + webbrowser.open(url) + return 0 + return 1 + + +def _translate_ch_to_exc(ch: str) -> t.Optional[BaseException]: + if ch == "\x03": + raise KeyboardInterrupt() + + if ch == "\x04" and not WIN: # Unix-like, Ctrl+D + raise EOFError() + + if ch == "\x1a" and WIN: # Windows, Ctrl+Z + raise EOFError() + + return None + + +if WIN: + import msvcrt + + @contextlib.contextmanager + def raw_terminal() -> t.Iterator[int]: + yield -1 + + def getchar(echo: bool) -> str: + # The function `getch` will return a bytes object corresponding to + # the pressed character. Since Windows 10 build 1803, it will also + # return \x00 when called a second time after pressing a regular key. + # + # `getwch` does not share this probably-bugged behavior. Moreover, it + # returns a Unicode object by default, which is what we want. + # + # Either of these functions will return \x00 or \xe0 to indicate + # a special key, and you need to call the same function again to get + # the "rest" of the code. The fun part is that \u00e0 is + # "latin small letter a with grave", so if you type that on a French + # keyboard, you _also_ get a \xe0. + # E.g., consider the Up arrow. This returns \xe0 and then \x48. The + # resulting Unicode string reads as "a with grave" + "capital H". + # This is indistinguishable from when the user actually types + # "a with grave" and then "capital H". + # + # When \xe0 is returned, we assume it's part of a special-key sequence + # and call `getwch` again, but that means that when the user types + # the \u00e0 character, `getchar` doesn't return until a second + # character is typed. + # The alternative is returning immediately, but that would mess up + # cross-platform handling of arrow keys and others that start with + # \xe0. Another option is using `getch`, but then we can't reliably + # read non-ASCII characters, because return values of `getch` are + # limited to the current 8-bit codepage. + # + # Anyway, Click doesn't claim to do this Right(tm), and using `getwch` + # is doing the right thing in more situations than with `getch`. + func: t.Callable[[], str] + + if echo: + func = msvcrt.getwche # type: ignore + else: + func = msvcrt.getwch # type: ignore + + rv = func() + + if rv in ("\x00", "\xe0"): + # \x00 and \xe0 are control characters that indicate special key, + # see above. + rv += func() + + _translate_ch_to_exc(rv) + return rv + + +else: + import tty + import termios + + @contextlib.contextmanager + def raw_terminal() -> t.Iterator[int]: + f: t.Optional[t.TextIO] + fd: int + + if not isatty(sys.stdin): + f = open("/dev/tty") + fd = f.fileno() + else: + fd = sys.stdin.fileno() + f = None + + try: + old_settings = termios.tcgetattr(fd) + + try: + tty.setraw(fd) + yield fd + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + sys.stdout.flush() + + if f is not None: + f.close() + except termios.error: + pass + + def getchar(echo: bool) -> str: + with raw_terminal() as fd: + ch = os.read(fd, 32).decode(get_best_encoding(sys.stdin), "replace") + + if echo and isatty(sys.stdout): + sys.stdout.write(ch) + + _translate_ch_to_exc(ch) + return ch diff --git a/flask-app/venv/lib/python3.8/site-packages/click/_textwrap.py b/flask-app/venv/lib/python3.8/site-packages/click/_textwrap.py new file mode 100644 index 000000000..b47dcbd42 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/_textwrap.py @@ -0,0 +1,49 @@ +import textwrap +import typing as t +from contextlib import contextmanager + + +class TextWrapper(textwrap.TextWrapper): + def _handle_long_word( + self, + reversed_chunks: t.List[str], + cur_line: t.List[str], + cur_len: int, + width: int, + ) -> None: + space_left = max(width - cur_len, 1) + + if self.break_long_words: + last = reversed_chunks[-1] + cut = last[:space_left] + res = last[space_left:] + cur_line.append(cut) + reversed_chunks[-1] = res + elif not cur_line: + cur_line.append(reversed_chunks.pop()) + + @contextmanager + def extra_indent(self, indent: str) -> t.Iterator[None]: + old_initial_indent = self.initial_indent + old_subsequent_indent = self.subsequent_indent + self.initial_indent += indent + self.subsequent_indent += indent + + try: + yield + finally: + self.initial_indent = old_initial_indent + self.subsequent_indent = old_subsequent_indent + + def indent_only(self, text: str) -> str: + rv = [] + + for idx, line in enumerate(text.splitlines()): + indent = self.initial_indent + + if idx > 0: + indent = self.subsequent_indent + + rv.append(f"{indent}{line}") + + return "\n".join(rv) diff --git a/flask-app/venv/lib/python3.8/site-packages/click/_unicodefun.py b/flask-app/venv/lib/python3.8/site-packages/click/_unicodefun.py new file mode 100644 index 000000000..9cb30c384 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/_unicodefun.py @@ -0,0 +1,100 @@ +import codecs +import os +from gettext import gettext as _ + + +def _verify_python_env() -> None: + """Ensures that the environment is good for Unicode.""" + try: + from locale import getpreferredencoding + + fs_enc = codecs.lookup(getpreferredencoding()).name + except Exception: + fs_enc = "ascii" + + if fs_enc != "ascii": + return + + extra = [ + _( + "Click will abort further execution because Python was" + " configured to use ASCII as encoding for the environment." + " Consult https://click.palletsprojects.com/unicode-support/" + " for mitigation steps." + ) + ] + + if os.name == "posix": + import subprocess + + try: + rv = subprocess.Popen( + ["locale", "-a"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="ascii", + errors="replace", + ).communicate()[0] + except OSError: + rv = "" + + good_locales = set() + has_c_utf8 = False + + for line in rv.splitlines(): + locale = line.strip() + + if locale.lower().endswith((".utf-8", ".utf8")): + good_locales.add(locale) + + if locale.lower() in ("c.utf8", "c.utf-8"): + has_c_utf8 = True + + if not good_locales: + extra.append( + _( + "Additional information: on this system no suitable" + " UTF-8 locales were discovered. This most likely" + " requires resolving by reconfiguring the locale" + " system." + ) + ) + elif has_c_utf8: + extra.append( + _( + "This system supports the C.UTF-8 locale which is" + " recommended. You might be able to resolve your" + " issue by exporting the following environment" + " variables:" + ) + ) + extra.append(" export LC_ALL=C.UTF-8\n export LANG=C.UTF-8") + else: + extra.append( + _( + "This system lists some UTF-8 supporting locales" + " that you can pick from. The following suitable" + " locales were discovered: {locales}" + ).format(locales=", ".join(sorted(good_locales))) + ) + + bad_locale = None + + for env_locale in os.environ.get("LC_ALL"), os.environ.get("LANG"): + if env_locale and env_locale.lower().endswith((".utf-8", ".utf8")): + bad_locale = env_locale + + if env_locale is not None: + break + + if bad_locale is not None: + extra.append( + _( + "Click discovered that you exported a UTF-8 locale" + " but the locale system could not pick up from it" + " because it does not exist. The exported locale is" + " {locale!r} but it is not supported." + ).format(locale=bad_locale) + ) + + raise RuntimeError("\n\n".join(extra)) diff --git a/flask-app/venv/lib/python3.8/site-packages/click/_winconsole.py b/flask-app/venv/lib/python3.8/site-packages/click/_winconsole.py new file mode 100644 index 000000000..6b20df315 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/_winconsole.py @@ -0,0 +1,279 @@ +# This module is based on the excellent work by Adam Bartoš who +# provided a lot of what went into the implementation here in +# the discussion to issue1602 in the Python bug tracker. +# +# There are some general differences in regards to how this works +# compared to the original patches as we do not need to patch +# the entire interpreter but just work in our little world of +# echo and prompt. +import io +import sys +import time +import typing as t +from ctypes import byref +from ctypes import c_char +from ctypes import c_char_p +from ctypes import c_int +from ctypes import c_ssize_t +from ctypes import c_ulong +from ctypes import c_void_p +from ctypes import POINTER +from ctypes import py_object +from ctypes import Structure +from ctypes.wintypes import DWORD +from ctypes.wintypes import HANDLE +from ctypes.wintypes import LPCWSTR +from ctypes.wintypes import LPWSTR + +from ._compat import _NonClosingTextIOWrapper + +assert sys.platform == "win32" +import msvcrt # noqa: E402 +from ctypes import windll # noqa: E402 +from ctypes import WINFUNCTYPE # noqa: E402 + +c_ssize_p = POINTER(c_ssize_t) + +kernel32 = windll.kernel32 +GetStdHandle = kernel32.GetStdHandle +ReadConsoleW = kernel32.ReadConsoleW +WriteConsoleW = kernel32.WriteConsoleW +GetConsoleMode = kernel32.GetConsoleMode +GetLastError = kernel32.GetLastError +GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32)) +CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))( + ("CommandLineToArgvW", windll.shell32) +) +LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32)) + +STDIN_HANDLE = GetStdHandle(-10) +STDOUT_HANDLE = GetStdHandle(-11) +STDERR_HANDLE = GetStdHandle(-12) + +PyBUF_SIMPLE = 0 +PyBUF_WRITABLE = 1 + +ERROR_SUCCESS = 0 +ERROR_NOT_ENOUGH_MEMORY = 8 +ERROR_OPERATION_ABORTED = 995 + +STDIN_FILENO = 0 +STDOUT_FILENO = 1 +STDERR_FILENO = 2 + +EOF = b"\x1a" +MAX_BYTES_WRITTEN = 32767 + +try: + from ctypes import pythonapi +except ImportError: + # On PyPy we cannot get buffers so our ability to operate here is + # severely limited. + get_buffer = None +else: + + class Py_buffer(Structure): + _fields_ = [ + ("buf", c_void_p), + ("obj", py_object), + ("len", c_ssize_t), + ("itemsize", c_ssize_t), + ("readonly", c_int), + ("ndim", c_int), + ("format", c_char_p), + ("shape", c_ssize_p), + ("strides", c_ssize_p), + ("suboffsets", c_ssize_p), + ("internal", c_void_p), + ] + + PyObject_GetBuffer = pythonapi.PyObject_GetBuffer + PyBuffer_Release = pythonapi.PyBuffer_Release + + def get_buffer(obj, writable=False): + buf = Py_buffer() + flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE + PyObject_GetBuffer(py_object(obj), byref(buf), flags) + + try: + buffer_type = c_char * buf.len + return buffer_type.from_address(buf.buf) + finally: + PyBuffer_Release(byref(buf)) + + +class _WindowsConsoleRawIOBase(io.RawIOBase): + def __init__(self, handle): + self.handle = handle + + def isatty(self): + super().isatty() + return True + + +class _WindowsConsoleReader(_WindowsConsoleRawIOBase): + def readable(self): + return True + + def readinto(self, b): + bytes_to_be_read = len(b) + if not bytes_to_be_read: + return 0 + elif bytes_to_be_read % 2: + raise ValueError( + "cannot read odd number of bytes from UTF-16-LE encoded console" + ) + + buffer = get_buffer(b, writable=True) + code_units_to_be_read = bytes_to_be_read // 2 + code_units_read = c_ulong() + + rv = ReadConsoleW( + HANDLE(self.handle), + buffer, + code_units_to_be_read, + byref(code_units_read), + None, + ) + if GetLastError() == ERROR_OPERATION_ABORTED: + # wait for KeyboardInterrupt + time.sleep(0.1) + if not rv: + raise OSError(f"Windows error: {GetLastError()}") + + if buffer[0] == EOF: + return 0 + return 2 * code_units_read.value + + +class _WindowsConsoleWriter(_WindowsConsoleRawIOBase): + def writable(self): + return True + + @staticmethod + def _get_error_message(errno): + if errno == ERROR_SUCCESS: + return "ERROR_SUCCESS" + elif errno == ERROR_NOT_ENOUGH_MEMORY: + return "ERROR_NOT_ENOUGH_MEMORY" + return f"Windows error {errno}" + + def write(self, b): + bytes_to_be_written = len(b) + buf = get_buffer(b) + code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2 + code_units_written = c_ulong() + + WriteConsoleW( + HANDLE(self.handle), + buf, + code_units_to_be_written, + byref(code_units_written), + None, + ) + bytes_written = 2 * code_units_written.value + + if bytes_written == 0 and bytes_to_be_written > 0: + raise OSError(self._get_error_message(GetLastError())) + return bytes_written + + +class ConsoleStream: + def __init__(self, text_stream: t.TextIO, byte_stream: t.BinaryIO) -> None: + self._text_stream = text_stream + self.buffer = byte_stream + + @property + def name(self) -> str: + return self.buffer.name + + def write(self, x: t.AnyStr) -> int: + if isinstance(x, str): + return self._text_stream.write(x) + try: + self.flush() + except Exception: + pass + return self.buffer.write(x) + + def writelines(self, lines: t.Iterable[t.AnyStr]) -> None: + for line in lines: + self.write(line) + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._text_stream, name) + + def isatty(self) -> bool: + return self.buffer.isatty() + + def __repr__(self): + return f"" + + +def _get_text_stdin(buffer_stream: t.BinaryIO) -> t.TextIO: + text_stream = _NonClosingTextIOWrapper( + io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) + + +def _get_text_stdout(buffer_stream: t.BinaryIO) -> t.TextIO: + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) + + +def _get_text_stderr(buffer_stream: t.BinaryIO) -> t.TextIO: + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) + + +_stream_factories: t.Mapping[int, t.Callable[[t.BinaryIO], t.TextIO]] = { + 0: _get_text_stdin, + 1: _get_text_stdout, + 2: _get_text_stderr, +} + + +def _is_console(f: t.TextIO) -> bool: + if not hasattr(f, "fileno"): + return False + + try: + fileno = f.fileno() + except (OSError, io.UnsupportedOperation): + return False + + handle = msvcrt.get_osfhandle(fileno) + return bool(GetConsoleMode(handle, byref(DWORD()))) + + +def _get_windows_console_stream( + f: t.TextIO, encoding: t.Optional[str], errors: t.Optional[str] +) -> t.Optional[t.TextIO]: + if ( + get_buffer is not None + and encoding in {"utf-16-le", None} + and errors in {"strict", None} + and _is_console(f) + ): + func = _stream_factories.get(f.fileno()) + if func is not None: + b = getattr(f, "buffer", None) + + if b is None: + return None + + return func(b) diff --git a/flask-app/venv/lib/python3.8/site-packages/click/core.py b/flask-app/venv/lib/python3.8/site-packages/click/core.py new file mode 100644 index 000000000..f2263544a --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/core.py @@ -0,0 +1,2953 @@ +import enum +import errno +import os +import sys +import typing +import typing as t +from collections import abc +from contextlib import contextmanager +from contextlib import ExitStack +from functools import partial +from functools import update_wrapper +from gettext import gettext as _ +from gettext import ngettext +from itertools import repeat + +from . import types +from ._unicodefun import _verify_python_env +from .exceptions import Abort +from .exceptions import BadParameter +from .exceptions import ClickException +from .exceptions import Exit +from .exceptions import MissingParameter +from .exceptions import UsageError +from .formatting import HelpFormatter +from .formatting import join_options +from .globals import pop_context +from .globals import push_context +from .parser import _flag_needs_value +from .parser import OptionParser +from .parser import split_opt +from .termui import confirm +from .termui import prompt +from .termui import style +from .utils import _detect_program_name +from .utils import _expand_args +from .utils import echo +from .utils import make_default_short_help +from .utils import make_str +from .utils import PacifyFlushWrapper + +if t.TYPE_CHECKING: + import typing_extensions as te + from .shell_completion import CompletionItem + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) +V = t.TypeVar("V") + + +def _complete_visible_commands( + ctx: "Context", incomplete: str +) -> t.Iterator[t.Tuple[str, "Command"]]: + """List all the subcommands of a group that start with the + incomplete value and aren't hidden. + + :param ctx: Invocation context for the group. + :param incomplete: Value being completed. May be empty. + """ + multi = t.cast(MultiCommand, ctx.command) + + for name in multi.list_commands(ctx): + if name.startswith(incomplete): + command = multi.get_command(ctx, name) + + if command is not None and not command.hidden: + yield name, command + + +def _check_multicommand( + base_command: "MultiCommand", cmd_name: str, cmd: "Command", register: bool = False +) -> None: + if not base_command.chain or not isinstance(cmd, MultiCommand): + return + if register: + hint = ( + "It is not possible to add multi commands as children to" + " another multi command that is in chain mode." + ) + else: + hint = ( + "Found a multi command as subcommand to a multi command" + " that is in chain mode. This is not supported." + ) + raise RuntimeError( + f"{hint}. Command {base_command.name!r} is set to chain and" + f" {cmd_name!r} was added as a subcommand but it in itself is a" + f" multi command. ({cmd_name!r} is a {type(cmd).__name__}" + f" within a chained {type(base_command).__name__} named" + f" {base_command.name!r})." + ) + + +def batch(iterable: t.Iterable[V], batch_size: int) -> t.List[t.Tuple[V, ...]]: + return list(zip(*repeat(iter(iterable), batch_size))) + + +@contextmanager +def augment_usage_errors( + ctx: "Context", param: t.Optional["Parameter"] = None +) -> t.Iterator[None]: + """Context manager that attaches extra information to exceptions.""" + try: + yield + except BadParameter as e: + if e.ctx is None: + e.ctx = ctx + if param is not None and e.param is None: + e.param = param + raise + except UsageError as e: + if e.ctx is None: + e.ctx = ctx + raise + + +def iter_params_for_processing( + invocation_order: t.Sequence["Parameter"], + declaration_order: t.Sequence["Parameter"], +) -> t.List["Parameter"]: + """Given a sequence of parameters in the order as should be considered + for processing and an iterable of parameters that exist, this returns + a list in the correct order as they should be processed. + """ + + def sort_key(item: "Parameter") -> t.Tuple[bool, float]: + try: + idx: float = invocation_order.index(item) + except ValueError: + idx = float("inf") + + return not item.is_eager, idx + + return sorted(declaration_order, key=sort_key) + + +class ParameterSource(enum.Enum): + """This is an :class:`~enum.Enum` that indicates the source of a + parameter's value. + + Use :meth:`click.Context.get_parameter_source` to get the + source for a parameter by name. + + .. versionchanged:: 8.0 + Use :class:`~enum.Enum` and drop the ``validate`` method. + + .. versionchanged:: 8.0 + Added the ``PROMPT`` value. + """ + + COMMANDLINE = enum.auto() + """The value was provided by the command line args.""" + ENVIRONMENT = enum.auto() + """The value was provided with an environment variable.""" + DEFAULT = enum.auto() + """Used the default specified by the parameter.""" + DEFAULT_MAP = enum.auto() + """Used a default provided by :attr:`Context.default_map`.""" + PROMPT = enum.auto() + """Used a prompt to confirm a default or provide a value.""" + + +class Context: + """The context is a special internal object that holds state relevant + for the script execution at every single level. It's normally invisible + to commands unless they opt-in to getting access to it. + + The context is useful as it can pass internal objects around and can + control special execution features such as reading data from + environment variables. + + A context can be used as context manager in which case it will call + :meth:`close` on teardown. + + :param command: the command class for this context. + :param parent: the parent context. + :param info_name: the info name for this invocation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it is usually + the name of the script, for commands below it it's + the name of the script. + :param obj: an arbitrary object of user data. + :param auto_envvar_prefix: the prefix to use for automatic environment + variables. If this is `None` then reading + from environment variables is disabled. This + does not affect manually set environment + variables which are always read. + :param default_map: a dictionary (like object) with default values + for parameters. + :param terminal_width: the width of the terminal. The default is + inherit from parent context. If no context + defines the terminal width then auto + detection will be applied. + :param max_content_width: the maximum width for content rendered by + Click (this currently only affects help + pages). This defaults to 80 characters if + not overridden. In other words: even if the + terminal is larger than that, Click will not + format things wider than 80 characters by + default. In addition to that, formatters might + add some safety mapping on the right. + :param resilient_parsing: if this flag is enabled then Click will + parse without any interactivity or callback + invocation. Default values will also be + ignored. This is useful for implementing + things such as completion support. + :param allow_extra_args: if this is set to `True` then extra arguments + at the end will not raise an error and will be + kept on the context. The default is to inherit + from the command. + :param allow_interspersed_args: if this is set to `False` then options + and arguments cannot be mixed. The + default is to inherit from the command. + :param ignore_unknown_options: instructs click to ignore options it does + not know and keeps them for later + processing. + :param help_option_names: optionally a list of strings that define how + the default help parameter is named. The + default is ``['--help']``. + :param token_normalize_func: an optional function that is used to + normalize tokens (options, choices, + etc.). This for instance can be used to + implement case insensitive behavior. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are used in texts that Click prints which is by + default not the case. This for instance would affect + help output. + :param show_default: Show defaults for all options. If not set, + defaults to the value from a parent context. Overrides an + option's ``show_default`` argument. + + .. versionchanged:: 8.0 + The ``show_default`` parameter defaults to the value from the + parent context. + + .. versionchanged:: 7.1 + Added the ``show_default`` parameter. + + .. versionchanged:: 4.0 + Added the ``color``, ``ignore_unknown_options``, and + ``max_content_width`` parameters. + + .. versionchanged:: 3.0 + Added the ``allow_extra_args`` and ``allow_interspersed_args`` + parameters. + + .. versionchanged:: 2.0 + Added the ``resilient_parsing``, ``help_option_names``, and + ``token_normalize_func`` parameters. + """ + + #: The formatter class to create with :meth:`make_formatter`. + #: + #: .. versionadded:: 8.0 + formatter_class: t.Type["HelpFormatter"] = HelpFormatter + + def __init__( + self, + command: "Command", + parent: t.Optional["Context"] = None, + info_name: t.Optional[str] = None, + obj: t.Optional[t.Any] = None, + auto_envvar_prefix: t.Optional[str] = None, + default_map: t.Optional[t.Dict[str, t.Any]] = None, + terminal_width: t.Optional[int] = None, + max_content_width: t.Optional[int] = None, + resilient_parsing: bool = False, + allow_extra_args: t.Optional[bool] = None, + allow_interspersed_args: t.Optional[bool] = None, + ignore_unknown_options: t.Optional[bool] = None, + help_option_names: t.Optional[t.List[str]] = None, + token_normalize_func: t.Optional[t.Callable[[str], str]] = None, + color: t.Optional[bool] = None, + show_default: t.Optional[bool] = None, + ) -> None: + #: the parent context or `None` if none exists. + self.parent = parent + #: the :class:`Command` for this context. + self.command = command + #: the descriptive information name + self.info_name = info_name + #: Map of parameter names to their parsed values. Parameters + #: with ``expose_value=False`` are not stored. + self.params: t.Dict[str, t.Any] = {} + #: the leftover arguments. + self.args: t.List[str] = [] + #: protected arguments. These are arguments that are prepended + #: to `args` when certain parsing scenarios are encountered but + #: must be never propagated to another arguments. This is used + #: to implement nested parsing. + self.protected_args: t.List[str] = [] + + if obj is None and parent is not None: + obj = parent.obj + + #: the user object stored. + self.obj: t.Any = obj + self._meta: t.Dict[str, t.Any] = getattr(parent, "meta", {}) + + #: A dictionary (-like object) with defaults for parameters. + if ( + default_map is None + and info_name is not None + and parent is not None + and parent.default_map is not None + ): + default_map = parent.default_map.get(info_name) + + self.default_map: t.Optional[t.Dict[str, t.Any]] = default_map + + #: This flag indicates if a subcommand is going to be executed. A + #: group callback can use this information to figure out if it's + #: being executed directly or because the execution flow passes + #: onwards to a subcommand. By default it's None, but it can be + #: the name of the subcommand to execute. + #: + #: If chaining is enabled this will be set to ``'*'`` in case + #: any commands are executed. It is however not possible to + #: figure out which ones. If you require this knowledge you + #: should use a :func:`result_callback`. + self.invoked_subcommand: t.Optional[str] = None + + if terminal_width is None and parent is not None: + terminal_width = parent.terminal_width + + #: The width of the terminal (None is autodetection). + self.terminal_width: t.Optional[int] = terminal_width + + if max_content_width is None and parent is not None: + max_content_width = parent.max_content_width + + #: The maximum width of formatted content (None implies a sensible + #: default which is 80 for most things). + self.max_content_width: t.Optional[int] = max_content_width + + if allow_extra_args is None: + allow_extra_args = command.allow_extra_args + + #: Indicates if the context allows extra args or if it should + #: fail on parsing. + #: + #: .. versionadded:: 3.0 + self.allow_extra_args = allow_extra_args + + if allow_interspersed_args is None: + allow_interspersed_args = command.allow_interspersed_args + + #: Indicates if the context allows mixing of arguments and + #: options or not. + #: + #: .. versionadded:: 3.0 + self.allow_interspersed_args: bool = allow_interspersed_args + + if ignore_unknown_options is None: + ignore_unknown_options = command.ignore_unknown_options + + #: Instructs click to ignore options that a command does not + #: understand and will store it on the context for later + #: processing. This is primarily useful for situations where you + #: want to call into external programs. Generally this pattern is + #: strongly discouraged because it's not possibly to losslessly + #: forward all arguments. + #: + #: .. versionadded:: 4.0 + self.ignore_unknown_options: bool = ignore_unknown_options + + if help_option_names is None: + if parent is not None: + help_option_names = parent.help_option_names + else: + help_option_names = ["--help"] + + #: The names for the help options. + self.help_option_names: t.List[str] = help_option_names + + if token_normalize_func is None and parent is not None: + token_normalize_func = parent.token_normalize_func + + #: An optional normalization function for tokens. This is + #: options, choices, commands etc. + self.token_normalize_func: t.Optional[ + t.Callable[[str], str] + ] = token_normalize_func + + #: Indicates if resilient parsing is enabled. In that case Click + #: will do its best to not cause any failures and default values + #: will be ignored. Useful for completion. + self.resilient_parsing: bool = resilient_parsing + + # If there is no envvar prefix yet, but the parent has one and + # the command on this level has a name, we can expand the envvar + # prefix automatically. + if auto_envvar_prefix is None: + if ( + parent is not None + and parent.auto_envvar_prefix is not None + and self.info_name is not None + ): + auto_envvar_prefix = ( + f"{parent.auto_envvar_prefix}_{self.info_name.upper()}" + ) + else: + auto_envvar_prefix = auto_envvar_prefix.upper() + + if auto_envvar_prefix is not None: + auto_envvar_prefix = auto_envvar_prefix.replace("-", "_") + + self.auto_envvar_prefix: t.Optional[str] = auto_envvar_prefix + + if color is None and parent is not None: + color = parent.color + + #: Controls if styling output is wanted or not. + self.color: t.Optional[bool] = color + + if show_default is None and parent is not None: + show_default = parent.show_default + + #: Show option default values when formatting help text. + self.show_default: t.Optional[bool] = show_default + + self._close_callbacks: t.List[t.Callable[[], t.Any]] = [] + self._depth = 0 + self._parameter_source: t.Dict[str, ParameterSource] = {} + self._exit_stack = ExitStack() + + def to_info_dict(self) -> t.Dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. This traverses the entire CLI + structure. + + .. code-block:: python + + with Context(cli) as ctx: + info = ctx.to_info_dict() + + .. versionadded:: 8.0 + """ + return { + "command": self.command.to_info_dict(self), + "info_name": self.info_name, + "allow_extra_args": self.allow_extra_args, + "allow_interspersed_args": self.allow_interspersed_args, + "ignore_unknown_options": self.ignore_unknown_options, + "auto_envvar_prefix": self.auto_envvar_prefix, + } + + def __enter__(self) -> "Context": + self._depth += 1 + push_context(self) + return self + + def __exit__(self, exc_type, exc_value, tb): # type: ignore + self._depth -= 1 + if self._depth == 0: + self.close() + pop_context() + + @contextmanager + def scope(self, cleanup: bool = True) -> t.Iterator["Context"]: + """This helper method can be used with the context object to promote + it to the current thread local (see :func:`get_current_context`). + The default behavior of this is to invoke the cleanup functions which + can be disabled by setting `cleanup` to `False`. The cleanup + functions are typically used for things such as closing file handles. + + If the cleanup is intended the context object can also be directly + used as a context manager. + + Example usage:: + + with ctx.scope(): + assert get_current_context() is ctx + + This is equivalent:: + + with ctx: + assert get_current_context() is ctx + + .. versionadded:: 5.0 + + :param cleanup: controls if the cleanup functions should be run or + not. The default is to run these functions. In + some situations the context only wants to be + temporarily pushed in which case this can be disabled. + Nested pushes automatically defer the cleanup. + """ + if not cleanup: + self._depth += 1 + try: + with self as rv: + yield rv + finally: + if not cleanup: + self._depth -= 1 + + @property + def meta(self) -> t.Dict[str, t.Any]: + """This is a dictionary which is shared with all the contexts + that are nested. It exists so that click utilities can store some + state here if they need to. It is however the responsibility of + that code to manage this dictionary well. + + The keys are supposed to be unique dotted strings. For instance + module paths are a good choice for it. What is stored in there is + irrelevant for the operation of click. However what is important is + that code that places data here adheres to the general semantics of + the system. + + Example usage:: + + LANG_KEY = f'{__name__}.lang' + + def set_language(value): + ctx = get_current_context() + ctx.meta[LANG_KEY] = value + + def get_language(): + return get_current_context().meta.get(LANG_KEY, 'en_US') + + .. versionadded:: 5.0 + """ + return self._meta + + def make_formatter(self) -> HelpFormatter: + """Creates the :class:`~click.HelpFormatter` for the help and + usage output. + + To quickly customize the formatter class used without overriding + this method, set the :attr:`formatter_class` attribute. + + .. versionchanged:: 8.0 + Added the :attr:`formatter_class` attribute. + """ + return self.formatter_class( + width=self.terminal_width, max_width=self.max_content_width + ) + + def with_resource(self, context_manager: t.ContextManager[V]) -> V: + """Register a resource as if it were used in a ``with`` + statement. The resource will be cleaned up when the context is + popped. + + Uses :meth:`contextlib.ExitStack.enter_context`. It calls the + resource's ``__enter__()`` method and returns the result. When + the context is popped, it closes the stack, which calls the + resource's ``__exit__()`` method. + + To register a cleanup function for something that isn't a + context manager, use :meth:`call_on_close`. Or use something + from :mod:`contextlib` to turn it into a context manager first. + + .. code-block:: python + + @click.group() + @click.option("--name") + @click.pass_context + def cli(ctx): + ctx.obj = ctx.with_resource(connect_db(name)) + + :param context_manager: The context manager to enter. + :return: Whatever ``context_manager.__enter__()`` returns. + + .. versionadded:: 8.0 + """ + return self._exit_stack.enter_context(context_manager) + + def call_on_close(self, f: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: + """Register a function to be called when the context tears down. + + This can be used to close resources opened during the script + execution. Resources that support Python's context manager + protocol which would be used in a ``with`` statement should be + registered with :meth:`with_resource` instead. + + :param f: The function to execute on teardown. + """ + return self._exit_stack.callback(f) + + def close(self) -> None: + """Invoke all close callbacks registered with + :meth:`call_on_close`, and exit all context managers entered + with :meth:`with_resource`. + """ + self._exit_stack.close() + # In case the context is reused, create a new exit stack. + self._exit_stack = ExitStack() + + @property + def command_path(self) -> str: + """The computed command path. This is used for the ``usage`` + information on the help page. It's automatically created by + combining the info names of the chain of contexts to the root. + """ + rv = "" + if self.info_name is not None: + rv = self.info_name + if self.parent is not None: + parent_command_path = [self.parent.command_path] + + if isinstance(self.parent.command, Command): + for param in self.parent.command.get_params(self): + parent_command_path.extend(param.get_usage_pieces(self)) + + rv = f"{' '.join(parent_command_path)} {rv}" + return rv.lstrip() + + def find_root(self) -> "Context": + """Finds the outermost context.""" + node = self + while node.parent is not None: + node = node.parent + return node + + def find_object(self, object_type: t.Type[V]) -> t.Optional[V]: + """Finds the closest object of a given type.""" + node: t.Optional["Context"] = self + + while node is not None: + if isinstance(node.obj, object_type): + return node.obj + + node = node.parent + + return None + + def ensure_object(self, object_type: t.Type[V]) -> V: + """Like :meth:`find_object` but sets the innermost object to a + new instance of `object_type` if it does not exist. + """ + rv = self.find_object(object_type) + if rv is None: + self.obj = rv = object_type() + return rv + + @typing.overload + def lookup_default( + self, name: str, call: "te.Literal[True]" = True + ) -> t.Optional[t.Any]: + ... + + @typing.overload + def lookup_default( + self, name: str, call: "te.Literal[False]" = ... + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: + ... + + def lookup_default(self, name: str, call: bool = True) -> t.Optional[t.Any]: + """Get the default for a parameter from :attr:`default_map`. + + :param name: Name of the parameter. + :param call: If the default is a callable, call it. Disable to + return the callable instead. + + .. versionchanged:: 8.0 + Added the ``call`` parameter. + """ + if self.default_map is not None: + value = self.default_map.get(name) + + if call and callable(value): + return value() + + return value + + return None + + def fail(self, message: str) -> "te.NoReturn": + """Aborts the execution of the program with a specific error + message. + + :param message: the error message to fail with. + """ + raise UsageError(message, self) + + def abort(self) -> "te.NoReturn": + """Aborts the script.""" + raise Abort() + + def exit(self, code: int = 0) -> "te.NoReturn": + """Exits the application with a given exit code.""" + raise Exit(code) + + def get_usage(self) -> str: + """Helper method to get formatted usage string for the current + context and command. + """ + return self.command.get_usage(self) + + def get_help(self) -> str: + """Helper method to get formatted help page for the current + context and command. + """ + return self.command.get_help(self) + + def _make_sub_context(self, command: "Command") -> "Context": + """Create a new context of the same type as this context, but + for a new command. + + :meta private: + """ + return type(self)(command, info_name=command.name, parent=self) + + def invoke( + __self, # noqa: B902 + __callback: t.Union["Command", t.Callable[..., t.Any]], + *args: t.Any, + **kwargs: t.Any, + ) -> t.Any: + """Invokes a command callback in exactly the way it expects. There + are two ways to invoke this method: + + 1. the first argument can be a callback and all other arguments and + keyword arguments are forwarded directly to the function. + 2. the first argument is a click command object. In that case all + arguments are forwarded as well but proper click parameters + (options and click arguments) must be keyword arguments and Click + will fill in defaults. + + Note that before Click 3.2 keyword arguments were not properly filled + in against the intention of this code and no context was created. For + more information about this change and why it was done in a bugfix + release see :ref:`upgrade-to-3.2`. + + .. versionchanged:: 8.0 + All ``kwargs`` are tracked in :attr:`params` so they will be + passed if :meth:`forward` is called at multiple levels. + """ + if isinstance(__callback, Command): + other_cmd = __callback + + if other_cmd.callback is None: + raise TypeError( + "The given command does not have a callback that can be invoked." + ) + else: + __callback = other_cmd.callback + + ctx = __self._make_sub_context(other_cmd) + + for param in other_cmd.params: + if param.name not in kwargs and param.expose_value: + kwargs[param.name] = param.type_cast_value( # type: ignore + ctx, param.get_default(ctx) + ) + + # Track all kwargs as params, so that forward() will pass + # them on in subsequent calls. + ctx.params.update(kwargs) + else: + ctx = __self + + with augment_usage_errors(__self): + with ctx: + return __callback(*args, **kwargs) + + def forward( + __self, __cmd: "Command", *args: t.Any, **kwargs: t.Any # noqa: B902 + ) -> t.Any: + """Similar to :meth:`invoke` but fills in default keyword + arguments from the current context if the other command expects + it. This cannot invoke callbacks directly, only other commands. + + .. versionchanged:: 8.0 + All ``kwargs`` are tracked in :attr:`params` so they will be + passed if ``forward`` is called at multiple levels. + """ + # Can only forward to other commands, not direct callbacks. + if not isinstance(__cmd, Command): + raise TypeError("Callback is not a command.") + + for param in __self.params: + if param not in kwargs: + kwargs[param] = __self.params[param] + + return __self.invoke(__cmd, *args, **kwargs) + + def set_parameter_source(self, name: str, source: ParameterSource) -> None: + """Set the source of a parameter. This indicates the location + from which the value of the parameter was obtained. + + :param name: The name of the parameter. + :param source: A member of :class:`~click.core.ParameterSource`. + """ + self._parameter_source[name] = source + + def get_parameter_source(self, name: str) -> t.Optional[ParameterSource]: + """Get the source of a parameter. This indicates the location + from which the value of the parameter was obtained. + + This can be useful for determining when a user specified a value + on the command line that is the same as the default value. It + will be :attr:`~click.core.ParameterSource.DEFAULT` only if the + value was actually taken from the default. + + :param name: The name of the parameter. + :rtype: ParameterSource + + .. versionchanged:: 8.0 + Returns ``None`` if the parameter was not provided from any + source. + """ + return self._parameter_source.get(name) + + +class BaseCommand: + """The base command implements the minimal API contract of commands. + Most code will never use this as it does not implement a lot of useful + functionality but it can act as the direct subclass of alternative + parsing methods that do not depend on the Click parser. + + For instance, this can be used to bridge Click and other systems like + argparse or docopt. + + Because base commands do not implement a lot of the API that other + parts of Click take for granted, they are not supported for all + operations. For instance, they cannot be used with the decorators + usually and they have no built-in callback system. + + .. versionchanged:: 2.0 + Added the `context_settings` parameter. + + :param name: the name of the command to use unless a group overrides it. + :param context_settings: an optional dictionary with defaults that are + passed to the context object. + """ + + #: The context class to create with :meth:`make_context`. + #: + #: .. versionadded:: 8.0 + context_class: t.Type[Context] = Context + #: the default for the :attr:`Context.allow_extra_args` flag. + allow_extra_args = False + #: the default for the :attr:`Context.allow_interspersed_args` flag. + allow_interspersed_args = True + #: the default for the :attr:`Context.ignore_unknown_options` flag. + ignore_unknown_options = False + + def __init__( + self, + name: t.Optional[str], + context_settings: t.Optional[t.Dict[str, t.Any]] = None, + ) -> None: + #: the name the command thinks it has. Upon registering a command + #: on a :class:`Group` the group will default the command name + #: with this information. You should instead use the + #: :class:`Context`\'s :attr:`~Context.info_name` attribute. + self.name = name + + if context_settings is None: + context_settings = {} + + #: an optional dictionary with defaults passed to the context. + self.context_settings: t.Dict[str, t.Any] = context_settings + + def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. This traverses the entire structure + below this command. + + Use :meth:`click.Context.to_info_dict` to traverse the entire + CLI structure. + + :param ctx: A :class:`Context` representing this command. + + .. versionadded:: 8.0 + """ + return {"name": self.name} + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.name}>" + + def get_usage(self, ctx: Context) -> str: + raise NotImplementedError("Base commands cannot get usage") + + def get_help(self, ctx: Context) -> str: + raise NotImplementedError("Base commands cannot get help") + + def make_context( + self, + info_name: t.Optional[str], + args: t.List[str], + parent: t.Optional[Context] = None, + **extra: t.Any, + ) -> Context: + """This function when given an info name and arguments will kick + off the parsing and create a new :class:`Context`. It does not + invoke the actual command callback though. + + To quickly customize the context class used without overriding + this method, set the :attr:`context_class` attribute. + + :param info_name: the info name for this invocation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it's usually + the name of the script, for commands below it it's + the name of the command. + :param args: the arguments to parse as list of strings. + :param parent: the parent context if available. + :param extra: extra keyword arguments forwarded to the context + constructor. + + .. versionchanged:: 8.0 + Added the :attr:`context_class` attribute. + """ + for key, value in self.context_settings.items(): + if key not in extra: + extra[key] = value + + ctx = self.context_class( + self, info_name=info_name, parent=parent, **extra # type: ignore + ) + + with ctx.scope(cleanup=False): + self.parse_args(ctx, args) + return ctx + + def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]: + """Given a context and a list of arguments this creates the parser + and parses the arguments, then modifies the context as necessary. + This is automatically invoked by :meth:`make_context`. + """ + raise NotImplementedError("Base commands do not know how to parse arguments.") + + def invoke(self, ctx: Context) -> t.Any: + """Given a context, this invokes the command. The default + implementation is raising a not implemented error. + """ + raise NotImplementedError("Base commands are not invokable by default") + + def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]: + """Return a list of completions for the incomplete value. Looks + at the names of chained multi-commands. + + Any command could be part of a chained multi-command, so sibling + commands are valid at any point during command completion. Other + command classes will return more completions. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + results: t.List["CompletionItem"] = [] + + while ctx.parent is not None: + ctx = ctx.parent + + if isinstance(ctx.command, MultiCommand) and ctx.command.chain: + results.extend( + CompletionItem(name, help=command.get_short_help_str()) + for name, command in _complete_visible_commands(ctx, incomplete) + if name not in ctx.protected_args + ) + + return results + + @typing.overload + def main( + self, + args: t.Optional[t.Sequence[str]] = None, + prog_name: t.Optional[str] = None, + complete_var: t.Optional[str] = None, + standalone_mode: "te.Literal[True]" = True, + **extra: t.Any, + ) -> "te.NoReturn": + ... + + @typing.overload + def main( + self, + args: t.Optional[t.Sequence[str]] = None, + prog_name: t.Optional[str] = None, + complete_var: t.Optional[str] = None, + standalone_mode: bool = ..., + **extra: t.Any, + ) -> t.Any: + ... + + def main( + self, + args: t.Optional[t.Sequence[str]] = None, + prog_name: t.Optional[str] = None, + complete_var: t.Optional[str] = None, + standalone_mode: bool = True, + windows_expand_args: bool = True, + **extra: t.Any, + ) -> t.Any: + """This is the way to invoke a script with all the bells and + whistles as a command line application. This will always terminate + the application after a call. If this is not wanted, ``SystemExit`` + needs to be caught. + + This method is also available by directly calling the instance of + a :class:`Command`. + + :param args: the arguments that should be used for parsing. If not + provided, ``sys.argv[1:]`` is used. + :param prog_name: the program name that should be used. By default + the program name is constructed by taking the file + name from ``sys.argv[0]``. + :param complete_var: the environment variable that controls the + bash completion support. The default is + ``"__COMPLETE"`` with prog_name in + uppercase. + :param standalone_mode: the default behavior is to invoke the script + in standalone mode. Click will then + handle exceptions and convert them into + error messages and the function will never + return but shut down the interpreter. If + this is set to `False` they will be + propagated to the caller and the return + value of this function is the return value + of :meth:`invoke`. + :param windows_expand_args: Expand glob patterns, user dir, and + env vars in command line args on Windows. + :param extra: extra keyword arguments are forwarded to the context + constructor. See :class:`Context` for more information. + + .. versionchanged:: 8.0.1 + Added the ``windows_expand_args`` parameter to allow + disabling command line arg expansion on Windows. + + .. versionchanged:: 8.0 + When taking arguments from ``sys.argv`` on Windows, glob + patterns, user dir, and env vars are expanded. + + .. versionchanged:: 3.0 + Added the ``standalone_mode`` parameter. + """ + # Verify that the environment is configured correctly, or reject + # further execution to avoid a broken script. + _verify_python_env() + + if args is None: + args = sys.argv[1:] + + if os.name == "nt" and windows_expand_args: + args = _expand_args(args) + else: + args = list(args) + + if prog_name is None: + prog_name = _detect_program_name() + + # Process shell completion requests and exit early. + self._main_shell_completion(extra, prog_name, complete_var) + + try: + try: + with self.make_context(prog_name, args, **extra) as ctx: + rv = self.invoke(ctx) + if not standalone_mode: + return rv + # it's not safe to `ctx.exit(rv)` here! + # note that `rv` may actually contain data like "1" which + # has obvious effects + # more subtle case: `rv=[None, None]` can come out of + # chained commands which all returned `None` -- so it's not + # even always obvious that `rv` indicates success/failure + # by its truthiness/falsiness + ctx.exit() + except (EOFError, KeyboardInterrupt): + echo(file=sys.stderr) + raise Abort() from None + except ClickException as e: + if not standalone_mode: + raise + e.show() + sys.exit(e.exit_code) + except OSError as e: + if e.errno == errno.EPIPE: + sys.stdout = t.cast(t.TextIO, PacifyFlushWrapper(sys.stdout)) + sys.stderr = t.cast(t.TextIO, PacifyFlushWrapper(sys.stderr)) + sys.exit(1) + else: + raise + except Exit as e: + if standalone_mode: + sys.exit(e.exit_code) + else: + # in non-standalone mode, return the exit code + # note that this is only reached if `self.invoke` above raises + # an Exit explicitly -- thus bypassing the check there which + # would return its result + # the results of non-standalone execution may therefore be + # somewhat ambiguous: if there are codepaths which lead to + # `ctx.exit(1)` and to `return 1`, the caller won't be able to + # tell the difference between the two + return e.exit_code + except Abort: + if not standalone_mode: + raise + echo(_("Aborted!"), file=sys.stderr) + sys.exit(1) + + def _main_shell_completion( + self, + ctx_args: t.Dict[str, t.Any], + prog_name: str, + complete_var: t.Optional[str] = None, + ) -> None: + """Check if the shell is asking for tab completion, process + that, then exit early. Called from :meth:`main` before the + program is invoked. + + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. Defaults to + ``_{PROG_NAME}_COMPLETE``. + """ + if complete_var is None: + complete_var = f"_{prog_name}_COMPLETE".replace("-", "_").upper() + + instruction = os.environ.get(complete_var) + + if not instruction: + return + + from .shell_completion import shell_complete + + rv = shell_complete(self, ctx_args, prog_name, complete_var, instruction) + sys.exit(rv) + + def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any: + """Alias for :meth:`main`.""" + return self.main(*args, **kwargs) + + +class Command(BaseCommand): + """Commands are the basic building block of command line interfaces in + Click. A basic command handles command line parsing and might dispatch + more parsing to commands nested below it. + + .. versionchanged:: 2.0 + Added the `context_settings` parameter. + .. versionchanged:: 8.0 + Added repr showing the command name + .. versionchanged:: 7.1 + Added the `no_args_is_help` parameter. + + :param name: the name of the command to use unless a group overrides it. + :param context_settings: an optional dictionary with defaults that are + passed to the context object. + :param callback: the callback to invoke. This is optional. + :param params: the parameters to register with this command. This can + be either :class:`Option` or :class:`Argument` objects. + :param help: the help string to use for this command. + :param epilog: like the help string but it's printed at the end of the + help page after everything else. + :param short_help: the short help to use for this command. This is + shown on the command listing of the parent command. + :param add_help_option: by default each command registers a ``--help`` + option. This can be disabled by this parameter. + :param no_args_is_help: this controls what happens if no arguments are + provided. This option is disabled by default. + If enabled this will add ``--help`` as argument + if no arguments are passed + :param hidden: hide this command from help outputs. + + :param deprecated: issues a message indicating that + the command is deprecated. + """ + + def __init__( + self, + name: t.Optional[str], + context_settings: t.Optional[t.Dict[str, t.Any]] = None, + callback: t.Optional[t.Callable[..., t.Any]] = None, + params: t.Optional[t.List["Parameter"]] = None, + help: t.Optional[str] = None, + epilog: t.Optional[str] = None, + short_help: t.Optional[str] = None, + options_metavar: t.Optional[str] = "[OPTIONS]", + add_help_option: bool = True, + no_args_is_help: bool = False, + hidden: bool = False, + deprecated: bool = False, + ) -> None: + super().__init__(name, context_settings) + #: the callback to execute when the command fires. This might be + #: `None` in which case nothing happens. + self.callback = callback + #: the list of parameters for this command in the order they + #: should show up in the help page and execute. Eager parameters + #: will automatically be handled before non eager ones. + self.params: t.List["Parameter"] = params or [] + + # if a form feed (page break) is found in the help text, truncate help + # text to the content preceding the first form feed + if help and "\f" in help: + help = help.split("\f", 1)[0] + + self.help = help + self.epilog = epilog + self.options_metavar = options_metavar + self.short_help = short_help + self.add_help_option = add_help_option + self.no_args_is_help = no_args_is_help + self.hidden = hidden + self.deprecated = deprecated + + def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict(ctx) + info_dict.update( + params=[param.to_info_dict() for param in self.get_params(ctx)], + help=self.help, + epilog=self.epilog, + short_help=self.short_help, + hidden=self.hidden, + deprecated=self.deprecated, + ) + return info_dict + + def get_usage(self, ctx: Context) -> str: + """Formats the usage line into a string and returns it. + + Calls :meth:`format_usage` internally. + """ + formatter = ctx.make_formatter() + self.format_usage(ctx, formatter) + return formatter.getvalue().rstrip("\n") + + def get_params(self, ctx: Context) -> t.List["Parameter"]: + rv = self.params + help_option = self.get_help_option(ctx) + + if help_option is not None: + rv = [*rv, help_option] + + return rv + + def format_usage(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the usage line into the formatter. + + This is a low-level method called by :meth:`get_usage`. + """ + pieces = self.collect_usage_pieces(ctx) + formatter.write_usage(ctx.command_path, " ".join(pieces)) + + def collect_usage_pieces(self, ctx: Context) -> t.List[str]: + """Returns all the pieces that go into the usage line and returns + it as a list of strings. + """ + rv = [self.options_metavar] if self.options_metavar else [] + + for param in self.get_params(ctx): + rv.extend(param.get_usage_pieces(ctx)) + + return rv + + def get_help_option_names(self, ctx: Context) -> t.List[str]: + """Returns the names for the help option.""" + all_names = set(ctx.help_option_names) + for param in self.params: + all_names.difference_update(param.opts) + all_names.difference_update(param.secondary_opts) + return list(all_names) + + def get_help_option(self, ctx: Context) -> t.Optional["Option"]: + """Returns the help option object.""" + help_options = self.get_help_option_names(ctx) + + if not help_options or not self.add_help_option: + return None + + def show_help(ctx: Context, param: "Parameter", value: str) -> None: + if value and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + return Option( + help_options, + is_flag=True, + is_eager=True, + expose_value=False, + callback=show_help, + help=_("Show this message and exit."), + ) + + def make_parser(self, ctx: Context) -> OptionParser: + """Creates the underlying option parser for this command.""" + parser = OptionParser(ctx) + for param in self.get_params(ctx): + param.add_to_parser(parser, ctx) + return parser + + def get_help(self, ctx: Context) -> str: + """Formats the help into a string and returns it. + + Calls :meth:`format_help` internally. + """ + formatter = ctx.make_formatter() + self.format_help(ctx, formatter) + return formatter.getvalue().rstrip("\n") + + def get_short_help_str(self, limit: int = 45) -> str: + """Gets short help for the command or makes it by shortening the + long help string. + """ + text = self.short_help or "" + + if not text and self.help: + text = make_default_short_help(self.help, limit) + + if self.deprecated: + text = _("(Deprecated) {text}").format(text=text) + + return text.strip() + + def format_help(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the help into the formatter if it exists. + + This is a low-level method called by :meth:`get_help`. + + This calls the following methods: + + - :meth:`format_usage` + - :meth:`format_help_text` + - :meth:`format_options` + - :meth:`format_epilog` + """ + self.format_usage(ctx, formatter) + self.format_help_text(ctx, formatter) + self.format_options(ctx, formatter) + self.format_epilog(ctx, formatter) + + def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the help text to the formatter if it exists.""" + text = self.help or "" + + if self.deprecated: + text = _("(Deprecated) {text}").format(text=text) + + if text: + formatter.write_paragraph() + + with formatter.indentation(): + formatter.write_text(text) + + def format_options(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes all the options into the formatter if they exist.""" + opts = [] + for param in self.get_params(ctx): + rv = param.get_help_record(ctx) + if rv is not None: + opts.append(rv) + + if opts: + with formatter.section(_("Options")): + formatter.write_dl(opts) + + def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the epilog into the formatter if it exists.""" + if self.epilog: + formatter.write_paragraph() + with formatter.indentation(): + formatter.write_text(self.epilog) + + def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]: + if not args and self.no_args_is_help and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + parser = self.make_parser(ctx) + opts, args, param_order = parser.parse_args(args=args) + + for param in iter_params_for_processing(param_order, self.get_params(ctx)): + value, args = param.handle_parse_result(ctx, opts, args) + + if args and not ctx.allow_extra_args and not ctx.resilient_parsing: + ctx.fail( + ngettext( + "Got unexpected extra argument ({args})", + "Got unexpected extra arguments ({args})", + len(args), + ).format(args=" ".join(map(str, args))) + ) + + ctx.args = args + return args + + def invoke(self, ctx: Context) -> t.Any: + """Given a context, this invokes the attached callback (if it exists) + in the right way. + """ + if self.deprecated: + message = _( + "DeprecationWarning: The command {name!r} is deprecated." + ).format(name=self.name) + echo(style(message, fg="red"), err=True) + + if self.callback is not None: + return ctx.invoke(self.callback, **ctx.params) + + def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]: + """Return a list of completions for the incomplete value. Looks + at the names of options and chained multi-commands. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + results: t.List["CompletionItem"] = [] + + if incomplete and not incomplete[0].isalnum(): + for param in self.get_params(ctx): + if ( + not isinstance(param, Option) + or param.hidden + or ( + not param.multiple + and ctx.get_parameter_source(param.name) # type: ignore + is ParameterSource.COMMANDLINE + ) + ): + continue + + results.extend( + CompletionItem(name, help=param.help) + for name in [*param.opts, *param.secondary_opts] + if name.startswith(incomplete) + ) + + results.extend(super().shell_complete(ctx, incomplete)) + return results + + +class MultiCommand(Command): + """A multi command is the basic implementation of a command that + dispatches to subcommands. The most common version is the + :class:`Group`. + + :param invoke_without_command: this controls how the multi command itself + is invoked. By default it's only invoked + if a subcommand is provided. + :param no_args_is_help: this controls what happens if no arguments are + provided. This option is enabled by default if + `invoke_without_command` is disabled or disabled + if it's enabled. If enabled this will add + ``--help`` as argument if no arguments are + passed. + :param subcommand_metavar: the string that is used in the documentation + to indicate the subcommand place. + :param chain: if this is set to `True` chaining of multiple subcommands + is enabled. This restricts the form of commands in that + they cannot have optional arguments but it allows + multiple commands to be chained together. + :param result_callback: The result callback to attach to this multi + command. This can be set or changed later with the + :meth:`result_callback` decorator. + """ + + allow_extra_args = True + allow_interspersed_args = False + + def __init__( + self, + name: t.Optional[str] = None, + invoke_without_command: bool = False, + no_args_is_help: t.Optional[bool] = None, + subcommand_metavar: t.Optional[str] = None, + chain: bool = False, + result_callback: t.Optional[t.Callable[..., t.Any]] = None, + **attrs: t.Any, + ) -> None: + super().__init__(name, **attrs) + + if no_args_is_help is None: + no_args_is_help = not invoke_without_command + + self.no_args_is_help = no_args_is_help + self.invoke_without_command = invoke_without_command + + if subcommand_metavar is None: + if chain: + subcommand_metavar = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..." + else: + subcommand_metavar = "COMMAND [ARGS]..." + + self.subcommand_metavar = subcommand_metavar + self.chain = chain + # The result callback that is stored. This can be set or + # overridden with the :func:`result_callback` decorator. + self._result_callback = result_callback + + if self.chain: + for param in self.params: + if isinstance(param, Argument) and not param.required: + raise RuntimeError( + "Multi commands in chain mode cannot have" + " optional arguments." + ) + + def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict(ctx) + commands = {} + + for name in self.list_commands(ctx): + command = self.get_command(ctx, name) + + if command is None: + continue + + sub_ctx = ctx._make_sub_context(command) + + with sub_ctx.scope(cleanup=False): + commands[name] = command.to_info_dict(sub_ctx) + + info_dict.update(commands=commands, chain=self.chain) + return info_dict + + def collect_usage_pieces(self, ctx: Context) -> t.List[str]: + rv = super().collect_usage_pieces(ctx) + rv.append(self.subcommand_metavar) + return rv + + def format_options(self, ctx: Context, formatter: HelpFormatter) -> None: + super().format_options(ctx, formatter) + self.format_commands(ctx, formatter) + + def result_callback(self, replace: bool = False) -> t.Callable[[F], F]: + """Adds a result callback to the command. By default if a + result callback is already registered this will chain them but + this can be disabled with the `replace` parameter. The result + callback is invoked with the return value of the subcommand + (or the list of return values from all subcommands if chaining + is enabled) as well as the parameters as they would be passed + to the main callback. + + Example:: + + @click.group() + @click.option('-i', '--input', default=23) + def cli(input): + return 42 + + @cli.result_callback() + def process_result(result, input): + return result + input + + :param replace: if set to `True` an already existing result + callback will be removed. + + .. versionchanged:: 8.0 + Renamed from ``resultcallback``. + + .. versionadded:: 3.0 + """ + + def decorator(f: F) -> F: + old_callback = self._result_callback + + if old_callback is None or replace: + self._result_callback = f + return f + + def function(__value, *args, **kwargs): # type: ignore + inner = old_callback(__value, *args, **kwargs) # type: ignore + return f(inner, *args, **kwargs) + + self._result_callback = rv = update_wrapper(t.cast(F, function), f) + return rv + + return decorator + + def resultcallback(self, replace: bool = False) -> t.Callable[[F], F]: + import warnings + + warnings.warn( + "'resultcallback' has been renamed to 'result_callback'." + " The old name will be removed in Click 8.1.", + DeprecationWarning, + stacklevel=2, + ) + return self.result_callback(replace=replace) + + def format_commands(self, ctx: Context, formatter: HelpFormatter) -> None: + """Extra format methods for multi methods that adds all the commands + after the options. + """ + commands = [] + for subcommand in self.list_commands(ctx): + cmd = self.get_command(ctx, subcommand) + # What is this, the tool lied about a command. Ignore it + if cmd is None: + continue + if cmd.hidden: + continue + + commands.append((subcommand, cmd)) + + # allow for 3 times the default spacing + if len(commands): + limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands) + + rows = [] + for subcommand, cmd in commands: + help = cmd.get_short_help_str(limit) + rows.append((subcommand, help)) + + if rows: + with formatter.section(_("Commands")): + formatter.write_dl(rows) + + def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]: + if not args and self.no_args_is_help and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + rest = super().parse_args(ctx, args) + + if self.chain: + ctx.protected_args = rest + ctx.args = [] + elif rest: + ctx.protected_args, ctx.args = rest[:1], rest[1:] + + return ctx.args + + def invoke(self, ctx: Context) -> t.Any: + def _process_result(value: t.Any) -> t.Any: + if self._result_callback is not None: + value = ctx.invoke(self._result_callback, value, **ctx.params) + return value + + if not ctx.protected_args: + if self.invoke_without_command: + # No subcommand was invoked, so the result callback is + # invoked with None for regular groups, or an empty list + # for chained groups. + with ctx: + super().invoke(ctx) + return _process_result([] if self.chain else None) + ctx.fail(_("Missing command.")) + + # Fetch args back out + args = [*ctx.protected_args, *ctx.args] + ctx.args = [] + ctx.protected_args = [] + + # If we're not in chain mode, we only allow the invocation of a + # single command but we also inform the current context about the + # name of the command to invoke. + if not self.chain: + # Make sure the context is entered so we do not clean up + # resources until the result processor has worked. + with ctx: + cmd_name, cmd, args = self.resolve_command(ctx, args) + assert cmd is not None + ctx.invoked_subcommand = cmd_name + super().invoke(ctx) + sub_ctx = cmd.make_context(cmd_name, args, parent=ctx) + with sub_ctx: + return _process_result(sub_ctx.command.invoke(sub_ctx)) + + # In chain mode we create the contexts step by step, but after the + # base command has been invoked. Because at that point we do not + # know the subcommands yet, the invoked subcommand attribute is + # set to ``*`` to inform the command that subcommands are executed + # but nothing else. + with ctx: + ctx.invoked_subcommand = "*" if args else None + super().invoke(ctx) + + # Otherwise we make every single context and invoke them in a + # chain. In that case the return value to the result processor + # is the list of all invoked subcommand's results. + contexts = [] + while args: + cmd_name, cmd, args = self.resolve_command(ctx, args) + assert cmd is not None + sub_ctx = cmd.make_context( + cmd_name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + ) + contexts.append(sub_ctx) + args, sub_ctx.args = sub_ctx.args, [] + + rv = [] + for sub_ctx in contexts: + with sub_ctx: + rv.append(sub_ctx.command.invoke(sub_ctx)) + return _process_result(rv) + + def resolve_command( + self, ctx: Context, args: t.List[str] + ) -> t.Tuple[t.Optional[str], t.Optional[Command], t.List[str]]: + cmd_name = make_str(args[0]) + original_cmd_name = cmd_name + + # Get the command + cmd = self.get_command(ctx, cmd_name) + + # If we can't find the command but there is a normalization + # function available, we try with that one. + if cmd is None and ctx.token_normalize_func is not None: + cmd_name = ctx.token_normalize_func(cmd_name) + cmd = self.get_command(ctx, cmd_name) + + # If we don't find the command we want to show an error message + # to the user that it was not provided. However, there is + # something else we should do: if the first argument looks like + # an option we want to kick off parsing again for arguments to + # resolve things like --help which now should go to the main + # place. + if cmd is None and not ctx.resilient_parsing: + if split_opt(cmd_name)[0]: + self.parse_args(ctx, ctx.args) + ctx.fail(_("No such command {name!r}.").format(name=original_cmd_name)) + return cmd_name if cmd else None, cmd, args[1:] + + def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]: + """Given a context and a command name, this returns a + :class:`Command` object if it exists or returns `None`. + """ + raise NotImplementedError + + def list_commands(self, ctx: Context) -> t.List[str]: + """Returns a list of subcommand names in the order they should + appear. + """ + return [] + + def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]: + """Return a list of completions for the incomplete value. Looks + at the names of options, subcommands, and chained + multi-commands. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + results = [ + CompletionItem(name, help=command.get_short_help_str()) + for name, command in _complete_visible_commands(ctx, incomplete) + ] + results.extend(super().shell_complete(ctx, incomplete)) + return results + + +class Group(MultiCommand): + """A group allows a command to have subcommands attached. This is + the most common way to implement nesting in Click. + + :param name: The name of the group command. + :param commands: A dict mapping names to :class:`Command` objects. + Can also be a list of :class:`Command`, which will use + :attr:`Command.name` to create the dict. + :param attrs: Other command arguments described in + :class:`MultiCommand`, :class:`Command`, and + :class:`BaseCommand`. + + .. versionchanged:: 8.0 + The ``commmands`` argument can be a list of command objects. + """ + + #: If set, this is used by the group's :meth:`command` decorator + #: as the default :class:`Command` class. This is useful to make all + #: subcommands use a custom command class. + #: + #: .. versionadded:: 8.0 + command_class: t.Optional[t.Type[Command]] = None + + #: If set, this is used by the group's :meth:`group` decorator + #: as the default :class:`Group` class. This is useful to make all + #: subgroups use a custom group class. + #: + #: If set to the special value :class:`type` (literally + #: ``group_class = type``), this group's class will be used as the + #: default class. This makes a custom group class continue to make + #: custom groups. + #: + #: .. versionadded:: 8.0 + group_class: t.Optional[t.Union[t.Type["Group"], t.Type[type]]] = None + # Literal[type] isn't valid, so use Type[type] + + def __init__( + self, + name: t.Optional[str] = None, + commands: t.Optional[t.Union[t.Dict[str, Command], t.Sequence[Command]]] = None, + **attrs: t.Any, + ) -> None: + super().__init__(name, **attrs) + + if commands is None: + commands = {} + elif isinstance(commands, abc.Sequence): + commands = {c.name: c for c in commands if c.name is not None} + + #: The registered subcommands by their exported names. + self.commands: t.Dict[str, Command] = commands + + def add_command(self, cmd: Command, name: t.Optional[str] = None) -> None: + """Registers another :class:`Command` with this group. If the name + is not provided, the name of the command is used. + """ + name = name or cmd.name + if name is None: + raise TypeError("Command has no name.") + _check_multicommand(self, name, cmd, register=True) + self.commands[name] = cmd + + def command( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], Command]: + """A shortcut decorator for declaring and attaching a command to + the group. This takes the same arguments as :func:`command` and + immediately registers the created command with this group by + calling :meth:`add_command`. + + To customize the command class used, set the + :attr:`command_class` attribute. + + .. versionchanged:: 8.0 + Added the :attr:`command_class` attribute. + """ + from .decorators import command + + if self.command_class is not None and "cls" not in kwargs: + kwargs["cls"] = self.command_class + + def decorator(f: t.Callable[..., t.Any]) -> Command: + cmd = command(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + + return decorator + + def group( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], "Group"]: + """A shortcut decorator for declaring and attaching a group to + the group. This takes the same arguments as :func:`group` and + immediately registers the created group with this group by + calling :meth:`add_command`. + + To customize the group class used, set the :attr:`group_class` + attribute. + + .. versionchanged:: 8.0 + Added the :attr:`group_class` attribute. + """ + from .decorators import group + + if self.group_class is not None and "cls" not in kwargs: + if self.group_class is type: + kwargs["cls"] = type(self) + else: + kwargs["cls"] = self.group_class + + def decorator(f: t.Callable[..., t.Any]) -> "Group": + cmd = group(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + + return decorator + + def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]: + return self.commands.get(cmd_name) + + def list_commands(self, ctx: Context) -> t.List[str]: + return sorted(self.commands) + + +class CommandCollection(MultiCommand): + """A command collection is a multi command that merges multiple multi + commands together into one. This is a straightforward implementation + that accepts a list of different multi commands as sources and + provides all the commands for each of them. + """ + + def __init__( + self, + name: t.Optional[str] = None, + sources: t.Optional[t.List[MultiCommand]] = None, + **attrs: t.Any, + ) -> None: + super().__init__(name, **attrs) + #: The list of registered multi commands. + self.sources: t.List[MultiCommand] = sources or [] + + def add_source(self, multi_cmd: MultiCommand) -> None: + """Adds a new multi command to the chain dispatcher.""" + self.sources.append(multi_cmd) + + def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]: + for source in self.sources: + rv = source.get_command(ctx, cmd_name) + + if rv is not None: + if self.chain: + _check_multicommand(self, cmd_name, rv) + + return rv + + return None + + def list_commands(self, ctx: Context) -> t.List[str]: + rv: t.Set[str] = set() + + for source in self.sources: + rv.update(source.list_commands(ctx)) + + return sorted(rv) + + +def _check_iter(value: t.Any) -> t.Iterator[t.Any]: + """Check if the value is iterable but not a string. Raises a type + error, or return an iterator over the value. + """ + if isinstance(value, str): + raise TypeError + + return iter(value) + + +class Parameter: + r"""A parameter to a command comes in two versions: they are either + :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently + not supported by design as some of the internals for parsing are + intentionally not finalized. + + Some settings are supported by both options and arguments. + + :param param_decls: the parameter declarations for this option or + argument. This is a list of flags or argument + names. + :param type: the type that should be used. Either a :class:`ParamType` + or a Python type. The later is converted into the former + automatically if supported. + :param required: controls if this is optional or not. + :param default: the default value if omitted. This can also be a callable, + in which case it's invoked when the default is needed + without any arguments. + :param callback: A function to further process or validate the value + after type conversion. It is called as ``f(ctx, param, value)`` + and must return the value. It is called for all sources, + including prompts. + :param nargs: the number of arguments to match. If not ``1`` the return + value is a tuple instead of single value. The default for + nargs is ``1`` (except if the type is a tuple, then it's + the arity of the tuple). If ``nargs=-1``, all remaining + parameters are collected. + :param metavar: how the value is represented in the help page. + :param expose_value: if this is `True` then the value is passed onwards + to the command callback and stored on the context, + otherwise it's skipped. + :param is_eager: eager values are processed before non eager ones. This + should not be set for arguments or it will inverse the + order of processing. + :param envvar: a string or list of strings that are environment variables + that should be checked. + :param shell_complete: A function that returns custom shell + completions. Used instead of the param's type completion if + given. Takes ``ctx, param, incomplete`` and must return a list + of :class:`~click.shell_completion.CompletionItem` or a list of + strings. + + .. versionchanged:: 8.0 + ``process_value`` validates required parameters and bounded + ``nargs``, and invokes the parameter callback before returning + the value. This allows the callback to validate prompts. + ``full_process_value`` is removed. + + .. versionchanged:: 8.0 + ``autocompletion`` is renamed to ``shell_complete`` and has new + semantics described above. The old name is deprecated and will + be removed in 8.1, until then it will be wrapped to match the + new requirements. + + .. versionchanged:: 8.0 + For ``multiple=True, nargs>1``, the default must be a list of + tuples. + + .. versionchanged:: 8.0 + Setting a default is no longer required for ``nargs>1``, it will + default to ``None``. ``multiple=True`` or ``nargs=-1`` will + default to ``()``. + + .. versionchanged:: 7.1 + Empty environment variables are ignored rather than taking the + empty string value. This makes it possible for scripts to clear + variables if they can't unset them. + + .. versionchanged:: 2.0 + Changed signature for parameter callback to also be passed the + parameter. The old callback format will still work, but it will + raise a warning to give you a chance to migrate the code easier. + """ + + param_type_name = "parameter" + + def __init__( + self, + param_decls: t.Optional[t.Sequence[str]] = None, + type: t.Optional[t.Union[types.ParamType, t.Any]] = None, + required: bool = False, + default: t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]] = None, + callback: t.Optional[t.Callable[[Context, "Parameter", t.Any], t.Any]] = None, + nargs: t.Optional[int] = None, + multiple: bool = False, + metavar: t.Optional[str] = None, + expose_value: bool = True, + is_eager: bool = False, + envvar: t.Optional[t.Union[str, t.Sequence[str]]] = None, + shell_complete: t.Optional[ + t.Callable[ + [Context, "Parameter", str], + t.Union[t.List["CompletionItem"], t.List[str]], + ] + ] = None, + autocompletion: t.Optional[ + t.Callable[ + [Context, t.List[str], str], t.List[t.Union[t.Tuple[str, str], str]] + ] + ] = None, + ) -> None: + self.name, self.opts, self.secondary_opts = self._parse_decls( + param_decls or (), expose_value + ) + self.type = types.convert_type(type, default) + + # Default nargs to what the type tells us if we have that + # information available. + if nargs is None: + if self.type.is_composite: + nargs = self.type.arity + else: + nargs = 1 + + self.required = required + self.callback = callback + self.nargs = nargs + self.multiple = multiple + self.expose_value = expose_value + self.default = default + self.is_eager = is_eager + self.metavar = metavar + self.envvar = envvar + + if autocompletion is not None: + import warnings + + warnings.warn( + "'autocompletion' is renamed to 'shell_complete'. The old name is" + " deprecated and will be removed in Click 8.1. See the docs about" + " 'Parameter' for information about new behavior.", + DeprecationWarning, + stacklevel=2, + ) + + def shell_complete( + ctx: Context, param: "Parameter", incomplete: str + ) -> t.List["CompletionItem"]: + from click.shell_completion import CompletionItem + + out = [] + + for c in autocompletion(ctx, [], incomplete): # type: ignore + if isinstance(c, tuple): + c = CompletionItem(c[0], help=c[1]) + elif isinstance(c, str): + c = CompletionItem(c) + + if c.value.startswith(incomplete): + out.append(c) + + return out + + self._custom_shell_complete = shell_complete + + if __debug__: + if self.type.is_composite and nargs != self.type.arity: + raise ValueError( + f"'nargs' must be {self.type.arity} (or None) for" + f" type {self.type!r}, but it was {nargs}." + ) + + # Skip no default or callable default. + check_default = default if not callable(default) else None + + if check_default is not None: + if multiple: + try: + # Only check the first value against nargs. + check_default = next(_check_iter(check_default), None) + except TypeError: + raise ValueError( + "'default' must be a list when 'multiple' is true." + ) from None + + # Can be None for multiple with empty default. + if nargs != 1 and check_default is not None: + try: + _check_iter(check_default) + except TypeError: + if multiple: + message = ( + "'default' must be a list of lists when 'multiple' is" + " true and 'nargs' != 1." + ) + else: + message = "'default' must be a list when 'nargs' != 1." + + raise ValueError(message) from None + + if nargs > 1 and len(check_default) != nargs: + subject = "item length" if multiple else "length" + raise ValueError( + f"'default' {subject} must match nargs={nargs}." + ) + + def to_info_dict(self) -> t.Dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. + + Use :meth:`click.Context.to_info_dict` to traverse the entire + CLI structure. + + .. versionadded:: 8.0 + """ + return { + "name": self.name, + "param_type_name": self.param_type_name, + "opts": self.opts, + "secondary_opts": self.secondary_opts, + "type": self.type.to_info_dict(), + "required": self.required, + "nargs": self.nargs, + "multiple": self.multiple, + "default": self.default, + "envvar": self.envvar, + } + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.name}>" + + def _parse_decls( + self, decls: t.Sequence[str], expose_value: bool + ) -> t.Tuple[t.Optional[str], t.List[str], t.List[str]]: + raise NotImplementedError() + + @property + def human_readable_name(self) -> str: + """Returns the human readable name of this parameter. This is the + same as the name for options, but the metavar for arguments. + """ + return self.name # type: ignore + + def make_metavar(self) -> str: + if self.metavar is not None: + return self.metavar + + metavar = self.type.get_metavar(self) + + if metavar is None: + metavar = self.type.name.upper() + + if self.nargs != 1: + metavar += "..." + + return metavar + + @typing.overload + def get_default( + self, ctx: Context, call: "te.Literal[True]" = True + ) -> t.Optional[t.Any]: + ... + + @typing.overload + def get_default( + self, ctx: Context, call: bool = ... + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: + ... + + def get_default( + self, ctx: Context, call: bool = True + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: + """Get the default for the parameter. Tries + :meth:`Context.lookup_default` first, then the local default. + + :param ctx: Current context. + :param call: If the default is a callable, call it. Disable to + return the callable instead. + + .. versionchanged:: 8.0.2 + Type casting is no longer performed when getting a default. + + .. versionchanged:: 8.0.1 + Type casting can fail in resilient parsing mode. Invalid + defaults will not prevent showing help text. + + .. versionchanged:: 8.0 + Looks at ``ctx.default_map`` first. + + .. versionchanged:: 8.0 + Added the ``call`` parameter. + """ + value = ctx.lookup_default(self.name, call=False) # type: ignore + + if value is None: + value = self.default + + if call and callable(value): + value = value() + + return value + + def add_to_parser(self, parser: OptionParser, ctx: Context) -> None: + raise NotImplementedError() + + def consume_value( + self, ctx: Context, opts: t.Mapping[str, t.Any] + ) -> t.Tuple[t.Any, ParameterSource]: + value = opts.get(self.name) # type: ignore + source = ParameterSource.COMMANDLINE + + if value is None: + value = self.value_from_envvar(ctx) + source = ParameterSource.ENVIRONMENT + + if value is None: + value = ctx.lookup_default(self.name) # type: ignore + source = ParameterSource.DEFAULT_MAP + + if value is None: + value = self.get_default(ctx) + source = ParameterSource.DEFAULT + + return value, source + + def type_cast_value(self, ctx: Context, value: t.Any) -> t.Any: + """Convert and validate a value against the option's + :attr:`type`, :attr:`multiple`, and :attr:`nargs`. + """ + if value is None: + return () if self.multiple or self.nargs == -1 else None + + def check_iter(value: t.Any) -> t.Iterator: + try: + return _check_iter(value) + except TypeError: + # This should only happen when passing in args manually, + # the parser should construct an iterable when parsing + # the command line. + raise BadParameter( + _("Value must be an iterable."), ctx=ctx, param=self + ) from None + + if self.nargs == 1 or self.type.is_composite: + convert: t.Callable[[t.Any], t.Any] = partial( + self.type, param=self, ctx=ctx + ) + elif self.nargs == -1: + + def convert(value: t.Any) -> t.Tuple: + return tuple(self.type(x, self, ctx) for x in check_iter(value)) + + else: # nargs > 1 + + def convert(value: t.Any) -> t.Tuple: + value = tuple(check_iter(value)) + + if len(value) != self.nargs: + raise BadParameter( + ngettext( + "Takes {nargs} values but 1 was given.", + "Takes {nargs} values but {len} were given.", + len(value), + ).format(nargs=self.nargs, len=len(value)), + ctx=ctx, + param=self, + ) + + return tuple(self.type(x, self, ctx) for x in value) + + if self.multiple: + return tuple(convert(x) for x in check_iter(value)) + + return convert(value) + + def value_is_missing(self, value: t.Any) -> bool: + if value is None: + return True + + if (self.nargs != 1 or self.multiple) and value == (): + return True + + return False + + def process_value(self, ctx: Context, value: t.Any) -> t.Any: + value = self.type_cast_value(ctx, value) + + if self.required and self.value_is_missing(value): + raise MissingParameter(ctx=ctx, param=self) + + if self.callback is not None: + value = self.callback(ctx, self, value) + + return value + + def resolve_envvar_value(self, ctx: Context) -> t.Optional[str]: + if self.envvar is None: + return None + + if isinstance(self.envvar, str): + rv = os.environ.get(self.envvar) + + if rv: + return rv + else: + for envvar in self.envvar: + rv = os.environ.get(envvar) + + if rv: + return rv + + return None + + def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]: + rv: t.Optional[t.Any] = self.resolve_envvar_value(ctx) + + if rv is not None and self.nargs != 1: + rv = self.type.split_envvar_value(rv) + + return rv + + def handle_parse_result( + self, ctx: Context, opts: t.Mapping[str, t.Any], args: t.List[str] + ) -> t.Tuple[t.Any, t.List[str]]: + with augment_usage_errors(ctx, param=self): + value, source = self.consume_value(ctx, opts) + ctx.set_parameter_source(self.name, source) # type: ignore + + try: + value = self.process_value(ctx, value) + except Exception: + if not ctx.resilient_parsing: + raise + + value = None + + if self.expose_value: + ctx.params[self.name] = value # type: ignore + + return value, args + + def get_help_record(self, ctx: Context) -> t.Optional[t.Tuple[str, str]]: + pass + + def get_usage_pieces(self, ctx: Context) -> t.List[str]: + return [] + + def get_error_hint(self, ctx: Context) -> str: + """Get a stringified version of the param for use in error messages to + indicate which param caused the error. + """ + hint_list = self.opts or [self.human_readable_name] + return " / ".join(f"'{x}'" for x in hint_list) + + def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionItem"]: + """Return a list of completions for the incomplete value. If a + ``shell_complete`` function was given during init, it is used. + Otherwise, the :attr:`type` + :meth:`~click.types.ParamType.shell_complete` function is used. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + if self._custom_shell_complete is not None: + results = self._custom_shell_complete(ctx, self, incomplete) + + if results and isinstance(results[0], str): + from click.shell_completion import CompletionItem + + results = [CompletionItem(c) for c in results] + + return t.cast(t.List["CompletionItem"], results) + + return self.type.shell_complete(ctx, self, incomplete) + + +class Option(Parameter): + """Options are usually optional values on the command line and + have some extra features that arguments don't have. + + All other parameters are passed onwards to the parameter constructor. + + :param show_default: controls if the default value should be shown on the + help page. Normally, defaults are not shown. If this + value is a string, it shows the string instead of the + value. This is particularly useful for dynamic options. + :param show_envvar: controls if an environment variable should be shown on + the help page. Normally, environment variables + are not shown. + :param prompt: if set to `True` or a non empty string then the user will be + prompted for input. If set to `True` the prompt will be the + option name capitalized. + :param confirmation_prompt: Prompt a second time to confirm the + value if it was prompted for. Can be set to a string instead of + ``True`` to customize the message. + :param prompt_required: If set to ``False``, the user will be + prompted for input only when the option was specified as a flag + without a value. + :param hide_input: if this is `True` then the input on the prompt will be + hidden from the user. This is useful for password + input. + :param is_flag: forces this option to act as a flag. The default is + auto detection. + :param flag_value: which value should be used for this flag if it's + enabled. This is set to a boolean automatically if + the option string contains a slash to mark two options. + :param multiple: if this is set to `True` then the argument is accepted + multiple times and recorded. This is similar to ``nargs`` + in how it works but supports arbitrary number of + arguments. + :param count: this flag makes an option increment an integer. + :param allow_from_autoenv: if this is enabled then the value of this + parameter will be pulled from an environment + variable in case a prefix is defined on the + context. + :param help: the help string. + :param hidden: hide this option from help outputs. + + .. versionchanged:: 8.0.1 + ``type`` is detected from ``flag_value`` if given. + """ + + param_type_name = "option" + + def __init__( + self, + param_decls: t.Optional[t.Sequence[str]] = None, + show_default: t.Union[bool, str] = False, + prompt: t.Union[bool, str] = False, + confirmation_prompt: t.Union[bool, str] = False, + prompt_required: bool = True, + hide_input: bool = False, + is_flag: t.Optional[bool] = None, + flag_value: t.Optional[t.Any] = None, + multiple: bool = False, + count: bool = False, + allow_from_autoenv: bool = True, + type: t.Optional[t.Union[types.ParamType, t.Any]] = None, + help: t.Optional[str] = None, + hidden: bool = False, + show_choices: bool = True, + show_envvar: bool = False, + **attrs: t.Any, + ) -> None: + default_is_missing = "default" not in attrs + super().__init__(param_decls, type=type, multiple=multiple, **attrs) + + if prompt is True: + if self.name is None: + raise TypeError("'name' is required with 'prompt=True'.") + + prompt_text: t.Optional[str] = self.name.replace("_", " ").capitalize() + elif prompt is False: + prompt_text = None + else: + prompt_text = t.cast(str, prompt) + + self.prompt = prompt_text + self.confirmation_prompt = confirmation_prompt + self.prompt_required = prompt_required + self.hide_input = hide_input + self.hidden = hidden + + # If prompt is enabled but not required, then the option can be + # used as a flag to indicate using prompt or flag_value. + self._flag_needs_value = self.prompt is not None and not self.prompt_required + + if is_flag is None: + if flag_value is not None: + # Implicitly a flag because flag_value was set. + is_flag = True + elif self._flag_needs_value: + # Not a flag, but when used as a flag it shows a prompt. + is_flag = False + else: + # Implicitly a flag because flag options were given. + is_flag = bool(self.secondary_opts) + elif is_flag is False and not self._flag_needs_value: + # Not a flag, and prompt is not enabled, can be used as a + # flag if flag_value is set. + self._flag_needs_value = flag_value is not None + + if is_flag and default_is_missing: + self.default: t.Union[t.Any, t.Callable[[], t.Any]] = False + + if flag_value is None: + flag_value = not self.default + + if is_flag and type is None: + # Re-guess the type from the flag value instead of the + # default. + self.type = types.convert_type(None, flag_value) + + self.is_flag: bool = is_flag + self.is_bool_flag = is_flag and isinstance(self.type, types.BoolParamType) + self.flag_value: t.Any = flag_value + + # Counting + self.count = count + if count: + if type is None: + self.type = types.IntRange(min=0) + if default_is_missing: + self.default = 0 + + self.allow_from_autoenv = allow_from_autoenv + self.help = help + self.show_default = show_default + self.show_choices = show_choices + self.show_envvar = show_envvar + + if __debug__: + if self.nargs == -1: + raise TypeError("nargs=-1 is not supported for options.") + + if self.prompt and self.is_flag and not self.is_bool_flag: + raise TypeError("'prompt' is not valid for non-boolean flag.") + + if not self.is_bool_flag and self.secondary_opts: + raise TypeError("Secondary flag is not valid for non-boolean flag.") + + if self.is_bool_flag and self.hide_input and self.prompt is not None: + raise TypeError( + "'prompt' with 'hide_input' is not valid for boolean flag." + ) + + if self.count: + if self.multiple: + raise TypeError("'count' is not valid with 'multiple'.") + + if self.is_flag: + raise TypeError("'count' is not valid with 'is_flag'.") + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update( + help=self.help, + prompt=self.prompt, + is_flag=self.is_flag, + flag_value=self.flag_value, + count=self.count, + hidden=self.hidden, + ) + return info_dict + + def _parse_decls( + self, decls: t.Sequence[str], expose_value: bool + ) -> t.Tuple[t.Optional[str], t.List[str], t.List[str]]: + opts = [] + secondary_opts = [] + name = None + possible_names = [] + + for decl in decls: + if decl.isidentifier(): + if name is not None: + raise TypeError(f"Name '{name}' defined twice") + name = decl + else: + split_char = ";" if decl[:1] == "/" else "/" + if split_char in decl: + first, second = decl.split(split_char, 1) + first = first.rstrip() + if first: + possible_names.append(split_opt(first)) + opts.append(first) + second = second.lstrip() + if second: + secondary_opts.append(second.lstrip()) + if first == second: + raise ValueError( + f"Boolean option {decl!r} cannot use the" + " same flag for true/false." + ) + else: + possible_names.append(split_opt(decl)) + opts.append(decl) + + if name is None and possible_names: + possible_names.sort(key=lambda x: -len(x[0])) # group long options first + name = possible_names[0][1].replace("-", "_").lower() + if not name.isidentifier(): + name = None + + if name is None: + if not expose_value: + return None, opts, secondary_opts + raise TypeError("Could not determine name for option") + + if not opts and not secondary_opts: + raise TypeError( + f"No options defined but a name was passed ({name})." + " Did you mean to declare an argument instead? Did" + f" you mean to pass '--{name}'?" + ) + + return name, opts, secondary_opts + + def add_to_parser(self, parser: OptionParser, ctx: Context) -> None: + if self.multiple: + action = "append" + elif self.count: + action = "count" + else: + action = "store" + + if self.is_flag: + action = f"{action}_const" + + if self.is_bool_flag and self.secondary_opts: + parser.add_option( + obj=self, opts=self.opts, dest=self.name, action=action, const=True + ) + parser.add_option( + obj=self, + opts=self.secondary_opts, + dest=self.name, + action=action, + const=False, + ) + else: + parser.add_option( + obj=self, + opts=self.opts, + dest=self.name, + action=action, + const=self.flag_value, + ) + else: + parser.add_option( + obj=self, + opts=self.opts, + dest=self.name, + action=action, + nargs=self.nargs, + ) + + def get_help_record(self, ctx: Context) -> t.Optional[t.Tuple[str, str]]: + if self.hidden: + return None + + any_prefix_is_slash = False + + def _write_opts(opts: t.Sequence[str]) -> str: + nonlocal any_prefix_is_slash + + rv, any_slashes = join_options(opts) + + if any_slashes: + any_prefix_is_slash = True + + if not self.is_flag and not self.count: + rv += f" {self.make_metavar()}" + + return rv + + rv = [_write_opts(self.opts)] + + if self.secondary_opts: + rv.append(_write_opts(self.secondary_opts)) + + help = self.help or "" + extra = [] + + if self.show_envvar: + envvar = self.envvar + + if envvar is None: + if ( + self.allow_from_autoenv + and ctx.auto_envvar_prefix is not None + and self.name is not None + ): + envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" + + if envvar is not None: + var_str = ( + envvar + if isinstance(envvar, str) + else ", ".join(str(d) for d in envvar) + ) + extra.append(_("env var: {var}").format(var=var_str)) + + # Temporarily enable resilient parsing to avoid type casting + # failing for the default. Might be possible to extend this to + # help formatting in general. + resilient = ctx.resilient_parsing + ctx.resilient_parsing = True + + try: + default_value = self.get_default(ctx, call=False) + finally: + ctx.resilient_parsing = resilient + + show_default_is_str = isinstance(self.show_default, str) + + if show_default_is_str or ( + default_value is not None and (self.show_default or ctx.show_default) + ): + if show_default_is_str: + default_string = f"({self.show_default})" + elif isinstance(default_value, (list, tuple)): + default_string = ", ".join(str(d) for d in default_value) + elif callable(default_value): + default_string = _("(dynamic)") + elif self.is_bool_flag and self.secondary_opts: + # For boolean flags that have distinct True/False opts, + # use the opt without prefix instead of the value. + default_string = split_opt( + (self.opts if self.default else self.secondary_opts)[0] + )[1] + else: + default_string = str(default_value) + + if default_string: + extra.append(_("default: {default}").format(default=default_string)) + + if ( + isinstance(self.type, types._NumberRangeBase) + # skip count with default range type + and not (self.count and self.type.min == 0 and self.type.max is None) + ): + range_str = self.type._describe_range() + + if range_str: + extra.append(range_str) + + if self.required: + extra.append(_("required")) + + if extra: + extra_str = "; ".join(extra) + help = f"{help} [{extra_str}]" if help else f"[{extra_str}]" + + return ("; " if any_prefix_is_slash else " / ").join(rv), help + + @typing.overload + def get_default( + self, ctx: Context, call: "te.Literal[True]" = True + ) -> t.Optional[t.Any]: + ... + + @typing.overload + def get_default( + self, ctx: Context, call: bool = ... + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: + ... + + def get_default( + self, ctx: Context, call: bool = True + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: + # If we're a non boolean flag our default is more complex because + # we need to look at all flags in the same group to figure out + # if we're the the default one in which case we return the flag + # value as default. + if self.is_flag and not self.is_bool_flag: + for param in ctx.command.params: + if param.name == self.name and param.default: + return param.flag_value # type: ignore + + return None + + return super().get_default(ctx, call=call) + + def prompt_for_value(self, ctx: Context) -> t.Any: + """This is an alternative flow that can be activated in the full + value processing if a value does not exist. It will prompt the + user until a valid value exists and then returns the processed + value as result. + """ + assert self.prompt is not None + + # Calculate the default before prompting anything to be stable. + default = self.get_default(ctx) + + # If this is a prompt for a flag we need to handle this + # differently. + if self.is_bool_flag: + return confirm(self.prompt, default) + + return prompt( + self.prompt, + default=default, + type=self.type, + hide_input=self.hide_input, + show_choices=self.show_choices, + confirmation_prompt=self.confirmation_prompt, + value_proc=lambda x: self.process_value(ctx, x), + ) + + def resolve_envvar_value(self, ctx: Context) -> t.Optional[str]: + rv = super().resolve_envvar_value(ctx) + + if rv is not None: + return rv + + if ( + self.allow_from_autoenv + and ctx.auto_envvar_prefix is not None + and self.name is not None + ): + envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" + rv = os.environ.get(envvar) + + return rv + + def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]: + rv: t.Optional[t.Any] = self.resolve_envvar_value(ctx) + + if rv is None: + return None + + value_depth = (self.nargs != 1) + bool(self.multiple) + + if value_depth > 0: + rv = self.type.split_envvar_value(rv) + + if self.multiple and self.nargs != 1: + rv = batch(rv, self.nargs) + + return rv + + def consume_value( + self, ctx: Context, opts: t.Mapping[str, "Parameter"] + ) -> t.Tuple[t.Any, ParameterSource]: + value, source = super().consume_value(ctx, opts) + + # The parser will emit a sentinel value if the option can be + # given as a flag without a value. This is different from None + # to distinguish from the flag not being given at all. + if value is _flag_needs_value: + if self.prompt is not None and not ctx.resilient_parsing: + value = self.prompt_for_value(ctx) + source = ParameterSource.PROMPT + else: + value = self.flag_value + source = ParameterSource.COMMANDLINE + + elif ( + self.multiple + and value is not None + and any(v is _flag_needs_value for v in value) + ): + value = [self.flag_value if v is _flag_needs_value else v for v in value] + source = ParameterSource.COMMANDLINE + + # The value wasn't set, or used the param's default, prompt if + # prompting is enabled. + elif ( + source in {None, ParameterSource.DEFAULT} + and self.prompt is not None + and (self.required or self.prompt_required) + and not ctx.resilient_parsing + ): + value = self.prompt_for_value(ctx) + source = ParameterSource.PROMPT + + return value, source + + +class Argument(Parameter): + """Arguments are positional parameters to a command. They generally + provide fewer features than options but can have infinite ``nargs`` + and are required by default. + + All parameters are passed onwards to the parameter constructor. + """ + + param_type_name = "argument" + + def __init__( + self, + param_decls: t.Sequence[str], + required: t.Optional[bool] = None, + **attrs: t.Any, + ) -> None: + if required is None: + if attrs.get("default") is not None: + required = False + else: + required = attrs.get("nargs", 1) > 0 + + if "multiple" in attrs: + raise TypeError("__init__() got an unexpected keyword argument 'multiple'.") + + super().__init__(param_decls, required=required, **attrs) + + if __debug__: + if self.default is not None and self.nargs == -1: + raise TypeError("'default' is not supported for nargs=-1.") + + @property + def human_readable_name(self) -> str: + if self.metavar is not None: + return self.metavar + return self.name.upper() # type: ignore + + def make_metavar(self) -> str: + if self.metavar is not None: + return self.metavar + var = self.type.get_metavar(self) + if not var: + var = self.name.upper() # type: ignore + if not self.required: + var = f"[{var}]" + if self.nargs != 1: + var += "..." + return var + + def _parse_decls( + self, decls: t.Sequence[str], expose_value: bool + ) -> t.Tuple[t.Optional[str], t.List[str], t.List[str]]: + if not decls: + if not expose_value: + return None, [], [] + raise TypeError("Could not determine name for argument") + if len(decls) == 1: + name = arg = decls[0] + name = name.replace("-", "_").lower() + else: + raise TypeError( + "Arguments take exactly one parameter declaration, got" + f" {len(decls)}." + ) + return name, [arg], [] + + def get_usage_pieces(self, ctx: Context) -> t.List[str]: + return [self.make_metavar()] + + def get_error_hint(self, ctx: Context) -> str: + return f"'{self.make_metavar()}'" + + def add_to_parser(self, parser: OptionParser, ctx: Context) -> None: + parser.add_argument(dest=self.name, nargs=self.nargs, obj=self) diff --git a/flask-app/venv/lib/python3.8/site-packages/click/decorators.py b/flask-app/venv/lib/python3.8/site-packages/click/decorators.py new file mode 100644 index 000000000..f1cc005af --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/decorators.py @@ -0,0 +1,436 @@ +import inspect +import types +import typing as t +from functools import update_wrapper +from gettext import gettext as _ + +from .core import Argument +from .core import Command +from .core import Context +from .core import Group +from .core import Option +from .core import Parameter +from .globals import get_current_context +from .utils import echo + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) +FC = t.TypeVar("FC", t.Callable[..., t.Any], Command) + + +def pass_context(f: F) -> F: + """Marks a callback as wanting to receive the current context + object as first argument. + """ + + def new_func(*args, **kwargs): # type: ignore + return f(get_current_context(), *args, **kwargs) + + return update_wrapper(t.cast(F, new_func), f) + + +def pass_obj(f: F) -> F: + """Similar to :func:`pass_context`, but only pass the object on the + context onwards (:attr:`Context.obj`). This is useful if that object + represents the state of a nested system. + """ + + def new_func(*args, **kwargs): # type: ignore + return f(get_current_context().obj, *args, **kwargs) + + return update_wrapper(t.cast(F, new_func), f) + + +def make_pass_decorator( + object_type: t.Type, ensure: bool = False +) -> "t.Callable[[F], F]": + """Given an object type this creates a decorator that will work + similar to :func:`pass_obj` but instead of passing the object of the + current context, it will find the innermost context of type + :func:`object_type`. + + This generates a decorator that works roughly like this:: + + from functools import update_wrapper + + def decorator(f): + @pass_context + def new_func(ctx, *args, **kwargs): + obj = ctx.find_object(object_type) + return ctx.invoke(f, obj, *args, **kwargs) + return update_wrapper(new_func, f) + return decorator + + :param object_type: the type of the object to pass. + :param ensure: if set to `True`, a new object will be created and + remembered on the context if it's not there yet. + """ + + def decorator(f: F) -> F: + def new_func(*args, **kwargs): # type: ignore + ctx = get_current_context() + + if ensure: + obj = ctx.ensure_object(object_type) + else: + obj = ctx.find_object(object_type) + + if obj is None: + raise RuntimeError( + "Managed to invoke callback without a context" + f" object of type {object_type.__name__!r}" + " existing." + ) + + return ctx.invoke(f, obj, *args, **kwargs) + + return update_wrapper(t.cast(F, new_func), f) + + return decorator + + +def pass_meta_key( + key: str, *, doc_description: t.Optional[str] = None +) -> "t.Callable[[F], F]": + """Create a decorator that passes a key from + :attr:`click.Context.meta` as the first argument to the decorated + function. + + :param key: Key in ``Context.meta`` to pass. + :param doc_description: Description of the object being passed, + inserted into the decorator's docstring. Defaults to "the 'key' + key from Context.meta". + + .. versionadded:: 8.0 + """ + + def decorator(f: F) -> F: + def new_func(*args, **kwargs): # type: ignore + ctx = get_current_context() + obj = ctx.meta[key] + return ctx.invoke(f, obj, *args, **kwargs) + + return update_wrapper(t.cast(F, new_func), f) + + if doc_description is None: + doc_description = f"the {key!r} key from :attr:`click.Context.meta`" + + decorator.__doc__ = ( + f"Decorator that passes {doc_description} as the first argument" + " to the decorated function." + ) + return decorator + + +def _make_command( + f: F, + name: t.Optional[str], + attrs: t.MutableMapping[str, t.Any], + cls: t.Type[Command], +) -> Command: + if isinstance(f, Command): + raise TypeError("Attempted to convert a callback into a command twice.") + + try: + params = f.__click_params__ # type: ignore + params.reverse() + del f.__click_params__ # type: ignore + except AttributeError: + params = [] + + help = attrs.get("help") + + if help is None: + help = inspect.getdoc(f) + else: + help = inspect.cleandoc(help) + + attrs["help"] = help + return cls( + name=name or f.__name__.lower().replace("_", "-"), + callback=f, + params=params, + **attrs, + ) + + +def command( + name: t.Optional[str] = None, + cls: t.Optional[t.Type[Command]] = None, + **attrs: t.Any, +) -> t.Callable[[F], Command]: + r"""Creates a new :class:`Command` and uses the decorated function as + callback. This will also automatically attach all decorated + :func:`option`\s and :func:`argument`\s as parameters to the command. + + The name of the command defaults to the name of the function with + underscores replaced by dashes. If you want to change that, you can + pass the intended name as the first argument. + + All keyword arguments are forwarded to the underlying command class. + + Once decorated the function turns into a :class:`Command` instance + that can be invoked as a command line utility or be attached to a + command :class:`Group`. + + :param name: the name of the command. This defaults to the function + name with underscores replaced by dashes. + :param cls: the command class to instantiate. This defaults to + :class:`Command`. + """ + if cls is None: + cls = Command + + def decorator(f: t.Callable[..., t.Any]) -> Command: + cmd = _make_command(f, name, attrs, cls) # type: ignore + cmd.__doc__ = f.__doc__ + return cmd + + return decorator + + +def group(name: t.Optional[str] = None, **attrs: t.Any) -> t.Callable[[F], Group]: + """Creates a new :class:`Group` with a function as callback. This + works otherwise the same as :func:`command` just that the `cls` + parameter is set to :class:`Group`. + """ + attrs.setdefault("cls", Group) + return t.cast(Group, command(name, **attrs)) + + +def _param_memo(f: FC, param: Parameter) -> None: + if isinstance(f, Command): + f.params.append(param) + else: + if not hasattr(f, "__click_params__"): + f.__click_params__ = [] # type: ignore + + f.__click_params__.append(param) # type: ignore + + +def argument(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]: + """Attaches an argument to the command. All positional arguments are + passed as parameter declarations to :class:`Argument`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Argument` instance manually + and attaching it to the :attr:`Command.params` list. + + :param cls: the argument class to instantiate. This defaults to + :class:`Argument`. + """ + + def decorator(f: FC) -> FC: + ArgumentClass = attrs.pop("cls", Argument) + _param_memo(f, ArgumentClass(param_decls, **attrs)) + return f + + return decorator + + +def option(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]: + """Attaches an option to the command. All positional arguments are + passed as parameter declarations to :class:`Option`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Option` instance manually + and attaching it to the :attr:`Command.params` list. + + :param cls: the option class to instantiate. This defaults to + :class:`Option`. + """ + + def decorator(f: FC) -> FC: + # Issue 926, copy attrs, so pre-defined options can re-use the same cls= + option_attrs = attrs.copy() + + if "help" in option_attrs: + option_attrs["help"] = inspect.cleandoc(option_attrs["help"]) + OptionClass = option_attrs.pop("cls", Option) + _param_memo(f, OptionClass(param_decls, **option_attrs)) + return f + + return decorator + + +def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Add a ``--yes`` option which shows a prompt before continuing if + not passed. If the prompt is declined, the program will exit. + + :param param_decls: One or more option names. Defaults to the single + value ``"--yes"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + + def callback(ctx: Context, param: Parameter, value: bool) -> None: + if not value: + ctx.abort() + + if not param_decls: + param_decls = ("--yes",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("callback", callback) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("prompt", "Do you want to continue?") + kwargs.setdefault("help", "Confirm the action without prompting.") + return option(*param_decls, **kwargs) + + +def password_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Add a ``--password`` option which prompts for a password, hiding + input and asking to enter the value again for confirmation. + + :param param_decls: One or more option names. Defaults to the single + value ``"--password"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + if not param_decls: + param_decls = ("--password",) + + kwargs.setdefault("prompt", True) + kwargs.setdefault("confirmation_prompt", True) + kwargs.setdefault("hide_input", True) + return option(*param_decls, **kwargs) + + +def version_option( + version: t.Optional[str] = None, + *param_decls: str, + package_name: t.Optional[str] = None, + prog_name: t.Optional[str] = None, + message: t.Optional[str] = None, + **kwargs: t.Any, +) -> t.Callable[[FC], FC]: + """Add a ``--version`` option which immediately prints the version + number and exits the program. + + If ``version`` is not provided, Click will try to detect it using + :func:`importlib.metadata.version` to get the version for the + ``package_name``. On Python < 3.8, the ``importlib_metadata`` + backport must be installed. + + If ``package_name`` is not provided, Click will try to detect it by + inspecting the stack frames. This will be used to detect the + version, so it must match the name of the installed package. + + :param version: The version number to show. If not provided, Click + will try to detect it. + :param param_decls: One or more option names. Defaults to the single + value ``"--version"``. + :param package_name: The package name to detect the version from. If + not provided, Click will try to detect it. + :param prog_name: The name of the CLI to show in the message. If not + provided, it will be detected from the command. + :param message: The message to show. The values ``%(prog)s``, + ``%(package)s``, and ``%(version)s`` are available. Defaults to + ``"%(prog)s, version %(version)s"``. + :param kwargs: Extra arguments are passed to :func:`option`. + :raise RuntimeError: ``version`` could not be detected. + + .. versionchanged:: 8.0 + Add the ``package_name`` parameter, and the ``%(package)s`` + value for messages. + + .. versionchanged:: 8.0 + Use :mod:`importlib.metadata` instead of ``pkg_resources``. The + version is detected based on the package name, not the entry + point name. The Python package name must match the installed + package name, or be passed with ``package_name=``. + """ + if message is None: + message = _("%(prog)s, version %(version)s") + + if version is None and package_name is None: + frame = inspect.currentframe() + f_back = frame.f_back if frame is not None else None + f_globals = f_back.f_globals if f_back is not None else None + # break reference cycle + # https://docs.python.org/3/library/inspect.html#the-interpreter-stack + del frame + + if f_globals is not None: + package_name = f_globals.get("__name__") + + if package_name == "__main__": + package_name = f_globals.get("__package__") + + if package_name: + package_name = package_name.partition(".")[0] + + def callback(ctx: Context, param: Parameter, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + + nonlocal prog_name + nonlocal version + + if prog_name is None: + prog_name = ctx.find_root().info_name + + if version is None and package_name is not None: + metadata: t.Optional[types.ModuleType] + + try: + from importlib import metadata # type: ignore + except ImportError: + # Python < 3.8 + import importlib_metadata as metadata # type: ignore + + try: + version = metadata.version(package_name) # type: ignore + except metadata.PackageNotFoundError: # type: ignore + raise RuntimeError( + f"{package_name!r} is not installed. Try passing" + " 'package_name' instead." + ) from None + + if version is None: + raise RuntimeError( + f"Could not determine the version for {package_name!r} automatically." + ) + + echo( + t.cast(str, message) + % {"prog": prog_name, "package": package_name, "version": version}, + color=ctx.color, + ) + ctx.exit() + + if not param_decls: + param_decls = ("--version",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("is_eager", True) + kwargs.setdefault("help", _("Show the version and exit.")) + kwargs["callback"] = callback + return option(*param_decls, **kwargs) + + +def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Add a ``--help`` option which immediately prints the help page + and exits the program. + + This is usually unnecessary, as the ``--help`` option is added to + each command automatically unless ``add_help_option=False`` is + passed. + + :param param_decls: One or more option names. Defaults to the single + value ``"--help"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + + def callback(ctx: Context, param: Parameter, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + if not param_decls: + param_decls = ("--help",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("is_eager", True) + kwargs.setdefault("help", _("Show this message and exit.")) + kwargs["callback"] = callback + return option(*param_decls, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/click/exceptions.py b/flask-app/venv/lib/python3.8/site-packages/click/exceptions.py new file mode 100644 index 000000000..9e20b3eb5 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/exceptions.py @@ -0,0 +1,287 @@ +import os +import typing as t +from gettext import gettext as _ +from gettext import ngettext + +from ._compat import get_text_stderr +from .utils import echo + +if t.TYPE_CHECKING: + from .core import Context + from .core import Parameter + + +def _join_param_hints( + param_hint: t.Optional[t.Union[t.Sequence[str], str]] +) -> t.Optional[str]: + if param_hint is not None and not isinstance(param_hint, str): + return " / ".join(repr(x) for x in param_hint) + + return param_hint + + +class ClickException(Exception): + """An exception that Click can handle and show to the user.""" + + #: The exit code for this exception. + exit_code = 1 + + def __init__(self, message: str) -> None: + super().__init__(message) + self.message = message + + def format_message(self) -> str: + return self.message + + def __str__(self) -> str: + return self.message + + def show(self, file: t.Optional[t.IO] = None) -> None: + if file is None: + file = get_text_stderr() + + echo(_("Error: {message}").format(message=self.format_message()), file=file) + + +class UsageError(ClickException): + """An internal exception that signals a usage error. This typically + aborts any further handling. + + :param message: the error message to display. + :param ctx: optionally the context that caused this error. Click will + fill in the context automatically in some situations. + """ + + exit_code = 2 + + def __init__(self, message: str, ctx: t.Optional["Context"] = None) -> None: + super().__init__(message) + self.ctx = ctx + self.cmd = self.ctx.command if self.ctx else None + + def show(self, file: t.Optional[t.IO] = None) -> None: + if file is None: + file = get_text_stderr() + color = None + hint = "" + if ( + self.ctx is not None + and self.ctx.command.get_help_option(self.ctx) is not None + ): + hint = _("Try '{command} {option}' for help.").format( + command=self.ctx.command_path, option=self.ctx.help_option_names[0] + ) + hint = f"{hint}\n" + if self.ctx is not None: + color = self.ctx.color + echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color) + echo( + _("Error: {message}").format(message=self.format_message()), + file=file, + color=color, + ) + + +class BadParameter(UsageError): + """An exception that formats out a standardized error message for a + bad parameter. This is useful when thrown from a callback or type as + Click will attach contextual information to it (for instance, which + parameter it is). + + .. versionadded:: 2.0 + + :param param: the parameter object that caused this error. This can + be left out, and Click will attach this info itself + if possible. + :param param_hint: a string that shows up as parameter name. This + can be used as alternative to `param` in cases + where custom validation should happen. If it is + a string it's used as such, if it's a list then + each item is quoted and separated. + """ + + def __init__( + self, + message: str, + ctx: t.Optional["Context"] = None, + param: t.Optional["Parameter"] = None, + param_hint: t.Optional[str] = None, + ) -> None: + super().__init__(message, ctx) + self.param = param + self.param_hint = param_hint + + def format_message(self) -> str: + if self.param_hint is not None: + param_hint = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) # type: ignore + else: + return _("Invalid value: {message}").format(message=self.message) + + return _("Invalid value for {param_hint}: {message}").format( + param_hint=_join_param_hints(param_hint), message=self.message + ) + + +class MissingParameter(BadParameter): + """Raised if click required an option or argument but it was not + provided when invoking the script. + + .. versionadded:: 4.0 + + :param param_type: a string that indicates the type of the parameter. + The default is to inherit the parameter type from + the given `param`. Valid values are ``'parameter'``, + ``'option'`` or ``'argument'``. + """ + + def __init__( + self, + message: t.Optional[str] = None, + ctx: t.Optional["Context"] = None, + param: t.Optional["Parameter"] = None, + param_hint: t.Optional[str] = None, + param_type: t.Optional[str] = None, + ) -> None: + super().__init__(message or "", ctx, param, param_hint) + self.param_type = param_type + + def format_message(self) -> str: + if self.param_hint is not None: + param_hint: t.Optional[str] = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) # type: ignore + else: + param_hint = None + + param_hint = _join_param_hints(param_hint) + param_hint = f" {param_hint}" if param_hint else "" + + param_type = self.param_type + if param_type is None and self.param is not None: + param_type = self.param.param_type_name + + msg = self.message + if self.param is not None: + msg_extra = self.param.type.get_missing_message(self.param) + if msg_extra: + if msg: + msg += f". {msg_extra}" + else: + msg = msg_extra + + msg = f" {msg}" if msg else "" + + # Translate param_type for known types. + if param_type == "argument": + missing = _("Missing argument") + elif param_type == "option": + missing = _("Missing option") + elif param_type == "parameter": + missing = _("Missing parameter") + else: + missing = _("Missing {param_type}").format(param_type=param_type) + + return f"{missing}{param_hint}.{msg}" + + def __str__(self) -> str: + if not self.message: + param_name = self.param.name if self.param else None + return _("Missing parameter: {param_name}").format(param_name=param_name) + else: + return self.message + + +class NoSuchOption(UsageError): + """Raised if click attempted to handle an option that does not + exist. + + .. versionadded:: 4.0 + """ + + def __init__( + self, + option_name: str, + message: t.Optional[str] = None, + possibilities: t.Optional[t.Sequence[str]] = None, + ctx: t.Optional["Context"] = None, + ) -> None: + if message is None: + message = _("No such option: {name}").format(name=option_name) + + super().__init__(message, ctx) + self.option_name = option_name + self.possibilities = possibilities + + def format_message(self) -> str: + if not self.possibilities: + return self.message + + possibility_str = ", ".join(sorted(self.possibilities)) + suggest = ngettext( + "Did you mean {possibility}?", + "(Possible options: {possibilities})", + len(self.possibilities), + ).format(possibility=possibility_str, possibilities=possibility_str) + return f"{self.message} {suggest}" + + +class BadOptionUsage(UsageError): + """Raised if an option is generally supplied but the use of the option + was incorrect. This is for instance raised if the number of arguments + for an option is not correct. + + .. versionadded:: 4.0 + + :param option_name: the name of the option being used incorrectly. + """ + + def __init__( + self, option_name: str, message: str, ctx: t.Optional["Context"] = None + ) -> None: + super().__init__(message, ctx) + self.option_name = option_name + + +class BadArgumentUsage(UsageError): + """Raised if an argument is generally supplied but the use of the argument + was incorrect. This is for instance raised if the number of values + for an argument is not correct. + + .. versionadded:: 6.0 + """ + + +class FileError(ClickException): + """Raised if a file cannot be opened.""" + + def __init__(self, filename: str, hint: t.Optional[str] = None) -> None: + if hint is None: + hint = _("unknown error") + + super().__init__(hint) + self.ui_filename = os.fsdecode(filename) + self.filename = filename + + def format_message(self) -> str: + return _("Could not open file {filename!r}: {message}").format( + filename=self.ui_filename, message=self.message + ) + + +class Abort(RuntimeError): + """An internal signalling exception that signals Click to abort.""" + + +class Exit(RuntimeError): + """An exception that indicates that the application should exit with some + status code. + + :param code: the status code to exit with. + """ + + __slots__ = ("exit_code",) + + def __init__(self, code: int = 0) -> None: + self.exit_code = code diff --git a/flask-app/venv/lib/python3.8/site-packages/click/formatting.py b/flask-app/venv/lib/python3.8/site-packages/click/formatting.py new file mode 100644 index 000000000..ddd2a2f82 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/formatting.py @@ -0,0 +1,301 @@ +import typing as t +from contextlib import contextmanager +from gettext import gettext as _ + +from ._compat import term_len +from .parser import split_opt + +# Can force a width. This is used by the test system +FORCED_WIDTH: t.Optional[int] = None + + +def measure_table(rows: t.Iterable[t.Tuple[str, str]]) -> t.Tuple[int, ...]: + widths: t.Dict[int, int] = {} + + for row in rows: + for idx, col in enumerate(row): + widths[idx] = max(widths.get(idx, 0), term_len(col)) + + return tuple(y for x, y in sorted(widths.items())) + + +def iter_rows( + rows: t.Iterable[t.Tuple[str, str]], col_count: int +) -> t.Iterator[t.Tuple[str, ...]]: + for row in rows: + yield row + ("",) * (col_count - len(row)) + + +def wrap_text( + text: str, + width: int = 78, + initial_indent: str = "", + subsequent_indent: str = "", + preserve_paragraphs: bool = False, +) -> str: + """A helper function that intelligently wraps text. By default, it + assumes that it operates on a single paragraph of text but if the + `preserve_paragraphs` parameter is provided it will intelligently + handle paragraphs (defined by two empty lines). + + If paragraphs are handled, a paragraph can be prefixed with an empty + line containing the ``\\b`` character (``\\x08``) to indicate that + no rewrapping should happen in that block. + + :param text: the text that should be rewrapped. + :param width: the maximum width for the text. + :param initial_indent: the initial indent that should be placed on the + first line as a string. + :param subsequent_indent: the indent string that should be placed on + each consecutive line. + :param preserve_paragraphs: if this flag is set then the wrapping will + intelligently handle paragraphs. + """ + from ._textwrap import TextWrapper + + text = text.expandtabs() + wrapper = TextWrapper( + width, + initial_indent=initial_indent, + subsequent_indent=subsequent_indent, + replace_whitespace=False, + ) + if not preserve_paragraphs: + return wrapper.fill(text) + + p: t.List[t.Tuple[int, bool, str]] = [] + buf: t.List[str] = [] + indent = None + + def _flush_par() -> None: + if not buf: + return + if buf[0].strip() == "\b": + p.append((indent or 0, True, "\n".join(buf[1:]))) + else: + p.append((indent or 0, False, " ".join(buf))) + del buf[:] + + for line in text.splitlines(): + if not line: + _flush_par() + indent = None + else: + if indent is None: + orig_len = term_len(line) + line = line.lstrip() + indent = orig_len - term_len(line) + buf.append(line) + _flush_par() + + rv = [] + for indent, raw, text in p: + with wrapper.extra_indent(" " * indent): + if raw: + rv.append(wrapper.indent_only(text)) + else: + rv.append(wrapper.fill(text)) + + return "\n\n".join(rv) + + +class HelpFormatter: + """This class helps with formatting text-based help pages. It's + usually just needed for very special internal cases, but it's also + exposed so that developers can write their own fancy outputs. + + At present, it always writes into memory. + + :param indent_increment: the additional increment for each level. + :param width: the width for the text. This defaults to the terminal + width clamped to a maximum of 78. + """ + + def __init__( + self, + indent_increment: int = 2, + width: t.Optional[int] = None, + max_width: t.Optional[int] = None, + ) -> None: + import shutil + + self.indent_increment = indent_increment + if max_width is None: + max_width = 80 + if width is None: + width = FORCED_WIDTH + if width is None: + width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50) + self.width = width + self.current_indent = 0 + self.buffer: t.List[str] = [] + + def write(self, string: str) -> None: + """Writes a unicode string into the internal buffer.""" + self.buffer.append(string) + + def indent(self) -> None: + """Increases the indentation.""" + self.current_indent += self.indent_increment + + def dedent(self) -> None: + """Decreases the indentation.""" + self.current_indent -= self.indent_increment + + def write_usage( + self, prog: str, args: str = "", prefix: t.Optional[str] = None + ) -> None: + """Writes a usage line into the buffer. + + :param prog: the program name. + :param args: whitespace separated list of arguments. + :param prefix: The prefix for the first line. Defaults to + ``"Usage: "``. + """ + if prefix is None: + prefix = f"{_('Usage:')} " + + usage_prefix = f"{prefix:>{self.current_indent}}{prog} " + text_width = self.width - self.current_indent + + if text_width >= (term_len(usage_prefix) + 20): + # The arguments will fit to the right of the prefix. + indent = " " * term_len(usage_prefix) + self.write( + wrap_text( + args, + text_width, + initial_indent=usage_prefix, + subsequent_indent=indent, + ) + ) + else: + # The prefix is too long, put the arguments on the next line. + self.write(usage_prefix) + self.write("\n") + indent = " " * (max(self.current_indent, term_len(prefix)) + 4) + self.write( + wrap_text( + args, text_width, initial_indent=indent, subsequent_indent=indent + ) + ) + + self.write("\n") + + def write_heading(self, heading: str) -> None: + """Writes a heading into the buffer.""" + self.write(f"{'':>{self.current_indent}}{heading}:\n") + + def write_paragraph(self) -> None: + """Writes a paragraph into the buffer.""" + if self.buffer: + self.write("\n") + + def write_text(self, text: str) -> None: + """Writes re-indented text into the buffer. This rewraps and + preserves paragraphs. + """ + indent = " " * self.current_indent + self.write( + wrap_text( + text, + self.width, + initial_indent=indent, + subsequent_indent=indent, + preserve_paragraphs=True, + ) + ) + self.write("\n") + + def write_dl( + self, + rows: t.Sequence[t.Tuple[str, str]], + col_max: int = 30, + col_spacing: int = 2, + ) -> None: + """Writes a definition list into the buffer. This is how options + and commands are usually formatted. + + :param rows: a list of two item tuples for the terms and values. + :param col_max: the maximum width of the first column. + :param col_spacing: the number of spaces between the first and + second column. + """ + rows = list(rows) + widths = measure_table(rows) + if len(widths) != 2: + raise TypeError("Expected two columns for definition list") + + first_col = min(widths[0], col_max) + col_spacing + + for first, second in iter_rows(rows, len(widths)): + self.write(f"{'':>{self.current_indent}}{first}") + if not second: + self.write("\n") + continue + if term_len(first) <= first_col - col_spacing: + self.write(" " * (first_col - term_len(first))) + else: + self.write("\n") + self.write(" " * (first_col + self.current_indent)) + + text_width = max(self.width - first_col - 2, 10) + wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True) + lines = wrapped_text.splitlines() + + if lines: + self.write(f"{lines[0]}\n") + + for line in lines[1:]: + self.write(f"{'':>{first_col + self.current_indent}}{line}\n") + else: + self.write("\n") + + @contextmanager + def section(self, name: str) -> t.Iterator[None]: + """Helpful context manager that writes a paragraph, a heading, + and the indents. + + :param name: the section name that is written as heading. + """ + self.write_paragraph() + self.write_heading(name) + self.indent() + try: + yield + finally: + self.dedent() + + @contextmanager + def indentation(self) -> t.Iterator[None]: + """A context manager that increases the indentation.""" + self.indent() + try: + yield + finally: + self.dedent() + + def getvalue(self) -> str: + """Returns the buffer contents.""" + return "".join(self.buffer) + + +def join_options(options: t.Sequence[str]) -> t.Tuple[str, bool]: + """Given a list of option strings this joins them in the most appropriate + way and returns them in the form ``(formatted_string, + any_prefix_is_slash)`` where the second item in the tuple is a flag that + indicates if any of the option prefixes was a slash. + """ + rv = [] + any_prefix_is_slash = False + + for opt in options: + prefix = split_opt(opt)[0] + + if prefix == "/": + any_prefix_is_slash = True + + rv.append((len(prefix), opt)) + + rv.sort(key=lambda x: x[0]) + return ", ".join(x[1] for x in rv), any_prefix_is_slash diff --git a/flask-app/venv/lib/python3.8/site-packages/click/globals.py b/flask-app/venv/lib/python3.8/site-packages/click/globals.py new file mode 100644 index 000000000..a7b0c9317 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/globals.py @@ -0,0 +1,69 @@ +import typing +import typing as t +from threading import local + +if t.TYPE_CHECKING: + import typing_extensions as te + from .core import Context + +_local = local() + + +@typing.overload +def get_current_context(silent: "te.Literal[False]" = False) -> "Context": + ... + + +@typing.overload +def get_current_context(silent: bool = ...) -> t.Optional["Context"]: + ... + + +def get_current_context(silent: bool = False) -> t.Optional["Context"]: + """Returns the current click context. This can be used as a way to + access the current context object from anywhere. This is a more implicit + alternative to the :func:`pass_context` decorator. This function is + primarily useful for helpers such as :func:`echo` which might be + interested in changing its behavior based on the current context. + + To push the current context, :meth:`Context.scope` can be used. + + .. versionadded:: 5.0 + + :param silent: if set to `True` the return value is `None` if no context + is available. The default behavior is to raise a + :exc:`RuntimeError`. + """ + try: + return t.cast("Context", _local.stack[-1]) + except (AttributeError, IndexError) as e: + if not silent: + raise RuntimeError("There is no active click context.") from e + + return None + + +def push_context(ctx: "Context") -> None: + """Pushes a new context to the current stack.""" + _local.__dict__.setdefault("stack", []).append(ctx) + + +def pop_context() -> None: + """Removes the top level from the stack.""" + _local.stack.pop() + + +def resolve_color_default(color: t.Optional[bool] = None) -> t.Optional[bool]: + """Internal helper to get the default value of the color flag. If a + value is passed it's returned unchanged, otherwise it's looked up from + the current context. + """ + if color is not None: + return color + + ctx = get_current_context(silent=True) + + if ctx is not None: + return ctx.color + + return None diff --git a/flask-app/venv/lib/python3.8/site-packages/click/parser.py b/flask-app/venv/lib/python3.8/site-packages/click/parser.py new file mode 100644 index 000000000..2d5a2ed7b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/parser.py @@ -0,0 +1,529 @@ +""" +This module started out as largely a copy paste from the stdlib's +optparse module with the features removed that we do not need from +optparse because we implement them in Click on a higher level (for +instance type handling, help formatting and a lot more). + +The plan is to remove more and more from here over time. + +The reason this is a different module and not optparse from the stdlib +is that there are differences in 2.x and 3.x about the error messages +generated and optparse in the stdlib uses gettext for no good reason +and might cause us issues. + +Click uses parts of optparse written by Gregory P. Ward and maintained +by the Python Software Foundation. This is limited to code in parser.py. + +Copyright 2001-2006 Gregory P. Ward. All rights reserved. +Copyright 2002-2006 Python Software Foundation. All rights reserved. +""" +# This code uses parts of optparse written by Gregory P. Ward and +# maintained by the Python Software Foundation. +# Copyright 2001-2006 Gregory P. Ward +# Copyright 2002-2006 Python Software Foundation +import typing as t +from collections import deque +from gettext import gettext as _ +from gettext import ngettext + +from .exceptions import BadArgumentUsage +from .exceptions import BadOptionUsage +from .exceptions import NoSuchOption +from .exceptions import UsageError + +if t.TYPE_CHECKING: + import typing_extensions as te + from .core import Argument as CoreArgument + from .core import Context + from .core import Option as CoreOption + from .core import Parameter as CoreParameter + +V = t.TypeVar("V") + +# Sentinel value that indicates an option was passed as a flag without a +# value but is not a flag option. Option.consume_value uses this to +# prompt or use the flag_value. +_flag_needs_value = object() + + +def _unpack_args( + args: t.Sequence[str], nargs_spec: t.Sequence[int] +) -> t.Tuple[t.Sequence[t.Union[str, t.Sequence[t.Optional[str]], None]], t.List[str]]: + """Given an iterable of arguments and an iterable of nargs specifications, + it returns a tuple with all the unpacked arguments at the first index + and all remaining arguments as the second. + + The nargs specification is the number of arguments that should be consumed + or `-1` to indicate that this position should eat up all the remainders. + + Missing items are filled with `None`. + """ + args = deque(args) + nargs_spec = deque(nargs_spec) + rv: t.List[t.Union[str, t.Tuple[t.Optional[str], ...], None]] = [] + spos: t.Optional[int] = None + + def _fetch(c: "te.Deque[V]") -> t.Optional[V]: + try: + if spos is None: + return c.popleft() + else: + return c.pop() + except IndexError: + return None + + while nargs_spec: + nargs = _fetch(nargs_spec) + + if nargs is None: + continue + + if nargs == 1: + rv.append(_fetch(args)) + elif nargs > 1: + x = [_fetch(args) for _ in range(nargs)] + + # If we're reversed, we're pulling in the arguments in reverse, + # so we need to turn them around. + if spos is not None: + x.reverse() + + rv.append(tuple(x)) + elif nargs < 0: + if spos is not None: + raise TypeError("Cannot have two nargs < 0") + + spos = len(rv) + rv.append(None) + + # spos is the position of the wildcard (star). If it's not `None`, + # we fill it with the remainder. + if spos is not None: + rv[spos] = tuple(args) + args = [] + rv[spos + 1 :] = reversed(rv[spos + 1 :]) + + return tuple(rv), list(args) + + +def split_opt(opt: str) -> t.Tuple[str, str]: + first = opt[:1] + if first.isalnum(): + return "", opt + if opt[1:2] == first: + return opt[:2], opt[2:] + return first, opt[1:] + + +def normalize_opt(opt: str, ctx: t.Optional["Context"]) -> str: + if ctx is None or ctx.token_normalize_func is None: + return opt + prefix, opt = split_opt(opt) + return f"{prefix}{ctx.token_normalize_func(opt)}" + + +def split_arg_string(string: str) -> t.List[str]: + """Split an argument string as with :func:`shlex.split`, but don't + fail if the string is incomplete. Ignores a missing closing quote or + incomplete escape sequence and uses the partial token as-is. + + .. code-block:: python + + split_arg_string("example 'my file") + ["example", "my file"] + + split_arg_string("example my\\") + ["example", "my"] + + :param string: String to split. + """ + import shlex + + lex = shlex.shlex(string, posix=True) + lex.whitespace_split = True + lex.commenters = "" + out = [] + + try: + for token in lex: + out.append(token) + except ValueError: + # Raised when end-of-string is reached in an invalid state. Use + # the partial token as-is. The quote or escape character is in + # lex.state, not lex.token. + out.append(lex.token) + + return out + + +class Option: + def __init__( + self, + obj: "CoreOption", + opts: t.Sequence[str], + dest: t.Optional[str], + action: t.Optional[str] = None, + nargs: int = 1, + const: t.Optional[t.Any] = None, + ): + self._short_opts = [] + self._long_opts = [] + self.prefixes = set() + + for opt in opts: + prefix, value = split_opt(opt) + if not prefix: + raise ValueError(f"Invalid start character for option ({opt})") + self.prefixes.add(prefix[0]) + if len(prefix) == 1 and len(value) == 1: + self._short_opts.append(opt) + else: + self._long_opts.append(opt) + self.prefixes.add(prefix) + + if action is None: + action = "store" + + self.dest = dest + self.action = action + self.nargs = nargs + self.const = const + self.obj = obj + + @property + def takes_value(self) -> bool: + return self.action in ("store", "append") + + def process(self, value: str, state: "ParsingState") -> None: + if self.action == "store": + state.opts[self.dest] = value # type: ignore + elif self.action == "store_const": + state.opts[self.dest] = self.const # type: ignore + elif self.action == "append": + state.opts.setdefault(self.dest, []).append(value) # type: ignore + elif self.action == "append_const": + state.opts.setdefault(self.dest, []).append(self.const) # type: ignore + elif self.action == "count": + state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore + else: + raise ValueError(f"unknown action '{self.action}'") + state.order.append(self.obj) + + +class Argument: + def __init__(self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1): + self.dest = dest + self.nargs = nargs + self.obj = obj + + def process( + self, + value: t.Union[t.Optional[str], t.Sequence[t.Optional[str]]], + state: "ParsingState", + ) -> None: + if self.nargs > 1: + assert value is not None + holes = sum(1 for x in value if x is None) + if holes == len(value): + value = None + elif holes != 0: + raise BadArgumentUsage( + _("Argument {name!r} takes {nargs} values.").format( + name=self.dest, nargs=self.nargs + ) + ) + + if self.nargs == -1 and self.obj.envvar is not None and value == (): + # Replace empty tuple with None so that a value from the + # environment may be tried. + value = None + + state.opts[self.dest] = value # type: ignore + state.order.append(self.obj) + + +class ParsingState: + def __init__(self, rargs: t.List[str]) -> None: + self.opts: t.Dict[str, t.Any] = {} + self.largs: t.List[str] = [] + self.rargs = rargs + self.order: t.List["CoreParameter"] = [] + + +class OptionParser: + """The option parser is an internal class that is ultimately used to + parse options and arguments. It's modelled after optparse and brings + a similar but vastly simplified API. It should generally not be used + directly as the high level Click classes wrap it for you. + + It's not nearly as extensible as optparse or argparse as it does not + implement features that are implemented on a higher level (such as + types or defaults). + + :param ctx: optionally the :class:`~click.Context` where this parser + should go with. + """ + + def __init__(self, ctx: t.Optional["Context"] = None) -> None: + #: The :class:`~click.Context` for this parser. This might be + #: `None` for some advanced use cases. + self.ctx = ctx + #: This controls how the parser deals with interspersed arguments. + #: If this is set to `False`, the parser will stop on the first + #: non-option. Click uses this to implement nested subcommands + #: safely. + self.allow_interspersed_args = True + #: This tells the parser how to deal with unknown options. By + #: default it will error out (which is sensible), but there is a + #: second mode where it will ignore it and continue processing + #: after shifting all the unknown options into the resulting args. + self.ignore_unknown_options = False + + if ctx is not None: + self.allow_interspersed_args = ctx.allow_interspersed_args + self.ignore_unknown_options = ctx.ignore_unknown_options + + self._short_opt: t.Dict[str, Option] = {} + self._long_opt: t.Dict[str, Option] = {} + self._opt_prefixes = {"-", "--"} + self._args: t.List[Argument] = [] + + def add_option( + self, + obj: "CoreOption", + opts: t.Sequence[str], + dest: t.Optional[str], + action: t.Optional[str] = None, + nargs: int = 1, + const: t.Optional[t.Any] = None, + ) -> None: + """Adds a new option named `dest` to the parser. The destination + is not inferred (unlike with optparse) and needs to be explicitly + provided. Action can be any of ``store``, ``store_const``, + ``append``, ``append_const`` or ``count``. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + opts = [normalize_opt(opt, self.ctx) for opt in opts] + option = Option(obj, opts, dest, action=action, nargs=nargs, const=const) + self._opt_prefixes.update(option.prefixes) + for opt in option._short_opts: + self._short_opt[opt] = option + for opt in option._long_opts: + self._long_opt[opt] = option + + def add_argument( + self, obj: "CoreArgument", dest: t.Optional[str], nargs: int = 1 + ) -> None: + """Adds a positional argument named `dest` to the parser. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + self._args.append(Argument(obj, dest=dest, nargs=nargs)) + + def parse_args( + self, args: t.List[str] + ) -> t.Tuple[t.Dict[str, t.Any], t.List[str], t.List["CoreParameter"]]: + """Parses positional arguments and returns ``(values, args, order)`` + for the parsed options and arguments as well as the leftover + arguments if there are any. The order is a list of objects as they + appear on the command line. If arguments appear multiple times they + will be memorized multiple times as well. + """ + state = ParsingState(args) + try: + self._process_args_for_options(state) + self._process_args_for_args(state) + except UsageError: + if self.ctx is None or not self.ctx.resilient_parsing: + raise + return state.opts, state.largs, state.order + + def _process_args_for_args(self, state: ParsingState) -> None: + pargs, args = _unpack_args( + state.largs + state.rargs, [x.nargs for x in self._args] + ) + + for idx, arg in enumerate(self._args): + arg.process(pargs[idx], state) + + state.largs = args + state.rargs = [] + + def _process_args_for_options(self, state: ParsingState) -> None: + while state.rargs: + arg = state.rargs.pop(0) + arglen = len(arg) + # Double dashes always handled explicitly regardless of what + # prefixes are valid. + if arg == "--": + return + elif arg[:1] in self._opt_prefixes and arglen > 1: + self._process_opts(arg, state) + elif self.allow_interspersed_args: + state.largs.append(arg) + else: + state.rargs.insert(0, arg) + return + + # Say this is the original argument list: + # [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)] + # ^ + # (we are about to process arg(i)). + # + # Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of + # [arg0, ..., arg(i-1)] (any options and their arguments will have + # been removed from largs). + # + # The while loop will usually consume 1 or more arguments per pass. + # If it consumes 1 (eg. arg is an option that takes no arguments), + # then after _process_arg() is done the situation is: + # + # largs = subset of [arg0, ..., arg(i)] + # rargs = [arg(i+1), ..., arg(N-1)] + # + # If allow_interspersed_args is false, largs will always be + # *empty* -- still a subset of [arg0, ..., arg(i-1)], but + # not a very interesting subset! + + def _match_long_opt( + self, opt: str, explicit_value: t.Optional[str], state: ParsingState + ) -> None: + if opt not in self._long_opt: + from difflib import get_close_matches + + possibilities = get_close_matches(opt, self._long_opt) + raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx) + + option = self._long_opt[opt] + if option.takes_value: + # At this point it's safe to modify rargs by injecting the + # explicit value, because no exception is raised in this + # branch. This means that the inserted value will be fully + # consumed. + if explicit_value is not None: + state.rargs.insert(0, explicit_value) + + value = self._get_value_from_state(opt, option, state) + + elif explicit_value is not None: + raise BadOptionUsage( + opt, _("Option {name!r} does not take a value.").format(name=opt) + ) + + else: + value = None + + option.process(value, state) + + def _match_short_opt(self, arg: str, state: ParsingState) -> None: + stop = False + i = 1 + prefix = arg[0] + unknown_options = [] + + for ch in arg[1:]: + opt = normalize_opt(f"{prefix}{ch}", self.ctx) + option = self._short_opt.get(opt) + i += 1 + + if not option: + if self.ignore_unknown_options: + unknown_options.append(ch) + continue + raise NoSuchOption(opt, ctx=self.ctx) + if option.takes_value: + # Any characters left in arg? Pretend they're the + # next arg, and stop consuming characters of arg. + if i < len(arg): + state.rargs.insert(0, arg[i:]) + stop = True + + value = self._get_value_from_state(opt, option, state) + + else: + value = None + + option.process(value, state) + + if stop: + break + + # If we got any unknown options we re-combinate the string of the + # remaining options and re-attach the prefix, then report that + # to the state as new larg. This way there is basic combinatorics + # that can be achieved while still ignoring unknown arguments. + if self.ignore_unknown_options and unknown_options: + state.largs.append(f"{prefix}{''.join(unknown_options)}") + + def _get_value_from_state( + self, option_name: str, option: Option, state: ParsingState + ) -> t.Any: + nargs = option.nargs + + if len(state.rargs) < nargs: + if option.obj._flag_needs_value: + # Option allows omitting the value. + value = _flag_needs_value + else: + raise BadOptionUsage( + option_name, + ngettext( + "Option {name!r} requires an argument.", + "Option {name!r} requires {nargs} arguments.", + nargs, + ).format(name=option_name, nargs=nargs), + ) + elif nargs == 1: + next_rarg = state.rargs[0] + + if ( + option.obj._flag_needs_value + and isinstance(next_rarg, str) + and next_rarg[:1] in self._opt_prefixes + and len(next_rarg) > 1 + ): + # The next arg looks like the start of an option, don't + # use it as the value if omitting the value is allowed. + value = _flag_needs_value + else: + value = state.rargs.pop(0) + else: + value = tuple(state.rargs[:nargs]) + del state.rargs[:nargs] + + return value + + def _process_opts(self, arg: str, state: ParsingState) -> None: + explicit_value = None + # Long option handling happens in two parts. The first part is + # supporting explicitly attached values. In any case, we will try + # to long match the option first. + if "=" in arg: + long_opt, explicit_value = arg.split("=", 1) + else: + long_opt = arg + norm_long_opt = normalize_opt(long_opt, self.ctx) + + # At this point we will match the (assumed) long option through + # the long option matching code. Note that this allows options + # like "-foo" to be matched as long options. + try: + self._match_long_opt(norm_long_opt, explicit_value, state) + except NoSuchOption: + # At this point the long option matching failed, and we need + # to try with short options. However there is a special rule + # which says, that if we have a two character options prefix + # (applies to "--foo" for instance), we do not dispatch to the + # short option code and will instead raise the no option + # error. + if arg[:2] not in self._opt_prefixes: + self._match_short_opt(arg, state) + return + + if not self.ignore_unknown_options: + raise + + state.largs.append(arg) diff --git a/flask-app/venv/lib/python3.8/site-packages/click/py.typed b/flask-app/venv/lib/python3.8/site-packages/click/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/flask-app/venv/lib/python3.8/site-packages/click/shell_completion.py b/flask-app/venv/lib/python3.8/site-packages/click/shell_completion.py new file mode 100644 index 000000000..cad080da6 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/shell_completion.py @@ -0,0 +1,581 @@ +import os +import re +import typing as t +from gettext import gettext as _ + +from .core import Argument +from .core import BaseCommand +from .core import Context +from .core import MultiCommand +from .core import Option +from .core import Parameter +from .core import ParameterSource +from .parser import split_arg_string +from .utils import echo + + +def shell_complete( + cli: BaseCommand, + ctx_args: t.Dict[str, t.Any], + prog_name: str, + complete_var: str, + instruction: str, +) -> int: + """Perform shell completion for the given CLI program. + + :param cli: Command being called. + :param ctx_args: Extra arguments to pass to + ``cli.make_context``. + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. + :param instruction: Value of ``complete_var`` with the completion + instruction and shell, in the form ``instruction_shell``. + :return: Status code to exit with. + """ + shell, _, instruction = instruction.partition("_") + comp_cls = get_completion_class(shell) + + if comp_cls is None: + return 1 + + comp = comp_cls(cli, ctx_args, prog_name, complete_var) + + if instruction == "source": + echo(comp.source()) + return 0 + + if instruction == "complete": + echo(comp.complete()) + return 0 + + return 1 + + +class CompletionItem: + """Represents a completion value and metadata about the value. The + default metadata is ``type`` to indicate special shell handling, + and ``help`` if a shell supports showing a help string next to the + value. + + Arbitrary parameters can be passed when creating the object, and + accessed using ``item.attr``. If an attribute wasn't passed, + accessing it returns ``None``. + + :param value: The completion suggestion. + :param type: Tells the shell script to provide special completion + support for the type. Click uses ``"dir"`` and ``"file"``. + :param help: String shown next to the value if supported. + :param kwargs: Arbitrary metadata. The built-in implementations + don't use this, but custom type completions paired with custom + shell support could use it. + """ + + __slots__ = ("value", "type", "help", "_info") + + def __init__( + self, + value: t.Any, + type: str = "plain", + help: t.Optional[str] = None, + **kwargs: t.Any, + ) -> None: + self.value = value + self.type = type + self.help = help + self._info = kwargs + + def __getattr__(self, name: str) -> t.Any: + return self._info.get(name) + + +# Only Bash >= 4.4 has the nosort option. +_SOURCE_BASH = """\ +%(complete_func)s() { + local IFS=$'\\n' + local response + + response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \ +%(complete_var)s=bash_complete $1) + + for completion in $response; do + IFS=',' read type value <<< "$completion" + + if [[ $type == 'dir' ]]; then + COMREPLY=() + compopt -o dirnames + elif [[ $type == 'file' ]]; then + COMREPLY=() + compopt -o default + elif [[ $type == 'plain' ]]; then + COMPREPLY+=($value) + fi + done + + return 0 +} + +%(complete_func)s_setup() { + complete -o nosort -F %(complete_func)s %(prog_name)s +} + +%(complete_func)s_setup; +""" + +_SOURCE_ZSH = """\ +#compdef %(prog_name)s + +%(complete_func)s() { + local -a completions + local -a completions_with_descriptions + local -a response + (( ! $+commands[%(prog_name)s] )) && return 1 + + response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \ +%(complete_var)s=zsh_complete %(prog_name)s)}") + + for type key descr in ${response}; do + if [[ "$type" == "plain" ]]; then + if [[ "$descr" == "_" ]]; then + completions+=("$key") + else + completions_with_descriptions+=("$key":"$descr") + fi + elif [[ "$type" == "dir" ]]; then + _path_files -/ + elif [[ "$type" == "file" ]]; then + _path_files -f + fi + done + + if [ -n "$completions_with_descriptions" ]; then + _describe -V unsorted completions_with_descriptions -U + fi + + if [ -n "$completions" ]; then + compadd -U -V unsorted -a completions + fi +} + +compdef %(complete_func)s %(prog_name)s; +""" + +_SOURCE_FISH = """\ +function %(complete_func)s; + set -l response; + + for value in (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \ +COMP_CWORD=(commandline -t) %(prog_name)s); + set response $response $value; + end; + + for completion in $response; + set -l metadata (string split "," $completion); + + if test $metadata[1] = "dir"; + __fish_complete_directories $metadata[2]; + else if test $metadata[1] = "file"; + __fish_complete_path $metadata[2]; + else if test $metadata[1] = "plain"; + echo $metadata[2]; + end; + end; +end; + +complete --no-files --command %(prog_name)s --arguments \ +"(%(complete_func)s)"; +""" + + +class ShellComplete: + """Base class for providing shell completion support. A subclass for + a given shell will override attributes and methods to implement the + completion instructions (``source`` and ``complete``). + + :param cli: Command being called. + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. + + .. versionadded:: 8.0 + """ + + name: t.ClassVar[str] + """Name to register the shell as with :func:`add_completion_class`. + This is used in completion instructions (``{name}_source`` and + ``{name}_complete``). + """ + + source_template: t.ClassVar[str] + """Completion script template formatted by :meth:`source`. This must + be provided by subclasses. + """ + + def __init__( + self, + cli: BaseCommand, + ctx_args: t.Dict[str, t.Any], + prog_name: str, + complete_var: str, + ) -> None: + self.cli = cli + self.ctx_args = ctx_args + self.prog_name = prog_name + self.complete_var = complete_var + + @property + def func_name(self) -> str: + """The name of the shell function defined by the completion + script. + """ + safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), re.ASCII) + return f"_{safe_name}_completion" + + def source_vars(self) -> t.Dict[str, t.Any]: + """Vars for formatting :attr:`source_template`. + + By default this provides ``complete_func``, ``complete_var``, + and ``prog_name``. + """ + return { + "complete_func": self.func_name, + "complete_var": self.complete_var, + "prog_name": self.prog_name, + } + + def source(self) -> str: + """Produce the shell script that defines the completion + function. By default this ``%``-style formats + :attr:`source_template` with the dict returned by + :meth:`source_vars`. + """ + return self.source_template % self.source_vars() + + def get_completion_args(self) -> t.Tuple[t.List[str], str]: + """Use the env vars defined by the shell script to return a + tuple of ``args, incomplete``. This must be implemented by + subclasses. + """ + raise NotImplementedError + + def get_completions( + self, args: t.List[str], incomplete: str + ) -> t.List[CompletionItem]: + """Determine the context and last complete command or parameter + from the complete args. Call that object's ``shell_complete`` + method to get the completions for the incomplete value. + + :param args: List of complete args before the incomplete value. + :param incomplete: Value being completed. May be empty. + """ + ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args) + obj, incomplete = _resolve_incomplete(ctx, args, incomplete) + return obj.shell_complete(ctx, incomplete) + + def format_completion(self, item: CompletionItem) -> str: + """Format a completion item into the form recognized by the + shell script. This must be implemented by subclasses. + + :param item: Completion item to format. + """ + raise NotImplementedError + + def complete(self) -> str: + """Produce the completion data to send back to the shell. + + By default this calls :meth:`get_completion_args`, gets the + completions, then calls :meth:`format_completion` for each + completion. + """ + args, incomplete = self.get_completion_args() + completions = self.get_completions(args, incomplete) + out = [self.format_completion(item) for item in completions] + return "\n".join(out) + + +class BashComplete(ShellComplete): + """Shell completion for Bash.""" + + name = "bash" + source_template = _SOURCE_BASH + + def _check_version(self) -> None: + import subprocess + + output = subprocess.run( + ["bash", "-c", "echo ${BASH_VERSION}"], stdout=subprocess.PIPE + ) + match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode()) + + if match is not None: + major, minor = match.groups() + + if major < "4" or major == "4" and minor < "4": + raise RuntimeError( + _( + "Shell completion is not supported for Bash" + " versions older than 4.4." + ) + ) + else: + raise RuntimeError( + _("Couldn't detect Bash version, shell completion is not supported.") + ) + + def source(self) -> str: + self._check_version() + return super().source() + + def get_completion_args(self) -> t.Tuple[t.List[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + cword = int(os.environ["COMP_CWORD"]) + args = cwords[1:cword] + + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + return args, incomplete + + def format_completion(self, item: CompletionItem) -> str: + return f"{item.type},{item.value}" + + +class ZshComplete(ShellComplete): + """Shell completion for Zsh.""" + + name = "zsh" + source_template = _SOURCE_ZSH + + def get_completion_args(self) -> t.Tuple[t.List[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + cword = int(os.environ["COMP_CWORD"]) + args = cwords[1:cword] + + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + return args, incomplete + + def format_completion(self, item: CompletionItem) -> str: + return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}" + + +class FishComplete(ShellComplete): + """Shell completion for Fish.""" + + name = "fish" + source_template = _SOURCE_FISH + + def get_completion_args(self) -> t.Tuple[t.List[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + incomplete = os.environ["COMP_CWORD"] + args = cwords[1:] + + # Fish stores the partial word in both COMP_WORDS and + # COMP_CWORD, remove it from complete args. + if incomplete and args and args[-1] == incomplete: + args.pop() + + return args, incomplete + + def format_completion(self, item: CompletionItem) -> str: + if item.help: + return f"{item.type},{item.value}\t{item.help}" + + return f"{item.type},{item.value}" + + +_available_shells: t.Dict[str, t.Type[ShellComplete]] = { + "bash": BashComplete, + "fish": FishComplete, + "zsh": ZshComplete, +} + + +def add_completion_class( + cls: t.Type[ShellComplete], name: t.Optional[str] = None +) -> None: + """Register a :class:`ShellComplete` subclass under the given name. + The name will be provided by the completion instruction environment + variable during completion. + + :param cls: The completion class that will handle completion for the + shell. + :param name: Name to register the class under. Defaults to the + class's ``name`` attribute. + """ + if name is None: + name = cls.name + + _available_shells[name] = cls + + +def get_completion_class(shell: str) -> t.Optional[t.Type[ShellComplete]]: + """Look up a registered :class:`ShellComplete` subclass by the name + provided by the completion instruction environment variable. If the + name isn't registered, returns ``None``. + + :param shell: Name the class is registered under. + """ + return _available_shells.get(shell) + + +def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool: + """Determine if the given parameter is an argument that can still + accept values. + + :param ctx: Invocation context for the command represented by the + parsed complete args. + :param param: Argument object being checked. + """ + if not isinstance(param, Argument): + return False + + assert param.name is not None + value = ctx.params[param.name] + return ( + param.nargs == -1 + or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE + or ( + param.nargs > 1 + and isinstance(value, (tuple, list)) + and len(value) < param.nargs + ) + ) + + +def _start_of_option(value: str) -> bool: + """Check if the value looks like the start of an option.""" + if not value: + return False + + c = value[0] + # Allow "/" since that starts a path. + return not c.isalnum() and c != "/" + + +def _is_incomplete_option(args: t.List[str], param: Parameter) -> bool: + """Determine if the given parameter is an option that needs a value. + + :param args: List of complete args before the incomplete value. + :param param: Option object being checked. + """ + if not isinstance(param, Option): + return False + + if param.is_flag: + return False + + last_option = None + + for index, arg in enumerate(reversed(args)): + if index + 1 > param.nargs: + break + + if _start_of_option(arg): + last_option = arg + + return last_option is not None and last_option in param.opts + + +def _resolve_context( + cli: BaseCommand, ctx_args: t.Dict[str, t.Any], prog_name: str, args: t.List[str] +) -> Context: + """Produce the context hierarchy starting with the command and + traversing the complete arguments. This only follows the commands, + it doesn't trigger input prompts or callbacks. + + :param cli: Command being called. + :param prog_name: Name of the executable in the shell. + :param args: List of complete args before the incomplete value. + """ + ctx_args["resilient_parsing"] = True + ctx = cli.make_context(prog_name, args.copy(), **ctx_args) + args = ctx.protected_args + ctx.args + + while args: + command = ctx.command + + if isinstance(command, MultiCommand): + if not command.chain: + name, cmd, args = command.resolve_command(ctx, args) + + if cmd is None: + return ctx + + ctx = cmd.make_context(name, args, parent=ctx, resilient_parsing=True) + args = ctx.protected_args + ctx.args + else: + while args: + name, cmd, args = command.resolve_command(ctx, args) + + if cmd is None: + return ctx + + sub_ctx = cmd.make_context( + name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + resilient_parsing=True, + ) + args = sub_ctx.args + + ctx = sub_ctx + args = [*sub_ctx.protected_args, *sub_ctx.args] + else: + break + + return ctx + + +def _resolve_incomplete( + ctx: Context, args: t.List[str], incomplete: str +) -> t.Tuple[t.Union[BaseCommand, Parameter], str]: + """Find the Click object that will handle the completion of the + incomplete value. Return the object and the incomplete value. + + :param ctx: Invocation context for the command represented by + the parsed complete args. + :param args: List of complete args before the incomplete value. + :param incomplete: Value being completed. May be empty. + """ + # Different shells treat an "=" between a long option name and + # value differently. Might keep the value joined, return the "=" + # as a separate item, or return the split name and value. Always + # split and discard the "=" to make completion easier. + if incomplete == "=": + incomplete = "" + elif "=" in incomplete and _start_of_option(incomplete): + name, _, incomplete = incomplete.partition("=") + args.append(name) + + # The "--" marker tells Click to stop treating values as options + # even if they start with the option character. If it hasn't been + # given and the incomplete arg looks like an option, the current + # command will provide option name completions. + if "--" not in args and _start_of_option(incomplete): + return ctx.command, incomplete + + params = ctx.command.get_params(ctx) + + # If the last complete arg is an option name with an incomplete + # value, the option will provide value completions. + for param in params: + if _is_incomplete_option(args, param): + return param, incomplete + + # It's not an option name or value. The first argument without a + # parsed value will provide value completions. + for param in params: + if _is_incomplete_argument(ctx, param): + return param, incomplete + + # There were no unparsed arguments, the command may be a group that + # will provide command name completions. + return ctx.command, incomplete diff --git a/flask-app/venv/lib/python3.8/site-packages/click/termui.py b/flask-app/venv/lib/python3.8/site-packages/click/termui.py new file mode 100644 index 000000000..cf8d5f132 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/termui.py @@ -0,0 +1,809 @@ +import inspect +import io +import itertools +import os +import sys +import typing +import typing as t +from gettext import gettext as _ + +from ._compat import isatty +from ._compat import strip_ansi +from ._compat import WIN +from .exceptions import Abort +from .exceptions import UsageError +from .globals import resolve_color_default +from .types import Choice +from .types import convert_type +from .types import ParamType +from .utils import echo +from .utils import LazyFile + +if t.TYPE_CHECKING: + from ._termui_impl import ProgressBar + +V = t.TypeVar("V") + +# The prompt functions to use. The doc tools currently override these +# functions to customize how they work. +visible_prompt_func: t.Callable[[str], str] = input + +_ansi_colors = { + "black": 30, + "red": 31, + "green": 32, + "yellow": 33, + "blue": 34, + "magenta": 35, + "cyan": 36, + "white": 37, + "reset": 39, + "bright_black": 90, + "bright_red": 91, + "bright_green": 92, + "bright_yellow": 93, + "bright_blue": 94, + "bright_magenta": 95, + "bright_cyan": 96, + "bright_white": 97, +} +_ansi_reset_all = "\033[0m" + + +def hidden_prompt_func(prompt: str) -> str: + import getpass + + return getpass.getpass(prompt) + + +def _build_prompt( + text: str, + suffix: str, + show_default: bool = False, + default: t.Optional[t.Any] = None, + show_choices: bool = True, + type: t.Optional[ParamType] = None, +) -> str: + prompt = text + if type is not None and show_choices and isinstance(type, Choice): + prompt += f" ({', '.join(map(str, type.choices))})" + if default is not None and show_default: + prompt = f"{prompt} [{_format_default(default)}]" + return f"{prompt}{suffix}" + + +def _format_default(default: t.Any) -> t.Any: + if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"): + return default.name # type: ignore + + return default + + +def prompt( + text: str, + default: t.Optional[t.Any] = None, + hide_input: bool = False, + confirmation_prompt: t.Union[bool, str] = False, + type: t.Optional[t.Union[ParamType, t.Any]] = None, + value_proc: t.Optional[t.Callable[[str], t.Any]] = None, + prompt_suffix: str = ": ", + show_default: bool = True, + err: bool = False, + show_choices: bool = True, +) -> t.Any: + """Prompts a user for input. This is a convenience function that can + be used to prompt a user for input later. + + If the user aborts the input by sending a interrupt signal, this + function will catch it and raise a :exc:`Abort` exception. + + :param text: the text to show for the prompt. + :param default: the default value to use if no input happens. If this + is not given it will prompt until it's aborted. + :param hide_input: if this is set to true then the input value will + be hidden. + :param confirmation_prompt: Prompt a second time to confirm the + value. Can be set to a string instead of ``True`` to customize + the message. + :param type: the type to use to check the value against. + :param value_proc: if this parameter is provided it's a function that + is invoked instead of the type conversion to + convert a value. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + :param show_choices: Show or hide choices if the passed type is a Choice. + For example if type is a Choice of either day or week, + show_choices is true and text is "Group by" then the + prompt will be "Group by (day, week): ". + + .. versionadded:: 8.0 + ``confirmation_prompt`` can be a custom string. + + .. versionadded:: 7.0 + Added the ``show_choices`` parameter. + + .. versionadded:: 6.0 + Added unicode support for cmd.exe on Windows. + + .. versionadded:: 4.0 + Added the `err` parameter. + + """ + + def prompt_func(text: str) -> str: + f = hidden_prompt_func if hide_input else visible_prompt_func + try: + # Write the prompt separately so that we get nice + # coloring through colorama on Windows + echo(text.rstrip(" "), nl=False, err=err) + # Echo a space to stdout to work around an issue where + # readline causes backspace to clear the whole line. + return f(" ") + except (KeyboardInterrupt, EOFError): + # getpass doesn't print a newline if the user aborts input with ^C. + # Allegedly this behavior is inherited from getpass(3). + # A doc bug has been filed at https://bugs.python.org/issue24711 + if hide_input: + echo(None, err=err) + raise Abort() from None + + if value_proc is None: + value_proc = convert_type(type, default) + + prompt = _build_prompt( + text, prompt_suffix, show_default, default, show_choices, type + ) + + if confirmation_prompt: + if confirmation_prompt is True: + confirmation_prompt = _("Repeat for confirmation") + + confirmation_prompt = t.cast(str, confirmation_prompt) + confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix) + + while True: + while True: + value = prompt_func(prompt) + if value: + break + elif default is not None: + value = default + break + try: + result = value_proc(value) + except UsageError as e: + if hide_input: + echo(_("Error: The value you entered was invalid."), err=err) + else: + echo(_("Error: {e.message}").format(e=e), err=err) # noqa: B306 + continue + if not confirmation_prompt: + return result + while True: + confirmation_prompt = t.cast(str, confirmation_prompt) + value2 = prompt_func(confirmation_prompt) + if value2: + break + if value == value2: + return result + echo(_("Error: The two entered values do not match."), err=err) + + +def confirm( + text: str, + default: t.Optional[bool] = False, + abort: bool = False, + prompt_suffix: str = ": ", + show_default: bool = True, + err: bool = False, +) -> bool: + """Prompts for confirmation (yes/no question). + + If the user aborts the input by sending a interrupt signal this + function will catch it and raise a :exc:`Abort` exception. + + :param text: the question to ask. + :param default: The default value to use when no input is given. If + ``None``, repeat until input is given. + :param abort: if this is set to `True` a negative answer aborts the + exception by raising :exc:`Abort`. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + + .. versionchanged:: 8.0 + Repeat until input is given if ``default`` is ``None``. + + .. versionadded:: 4.0 + Added the ``err`` parameter. + """ + prompt = _build_prompt( + text, + prompt_suffix, + show_default, + "y/n" if default is None else ("Y/n" if default else "y/N"), + ) + + while True: + try: + # Write the prompt separately so that we get nice + # coloring through colorama on Windows + echo(prompt.rstrip(" "), nl=False, err=err) + # Echo a space to stdout to work around an issue where + # readline causes backspace to clear the whole line. + value = visible_prompt_func(" ").lower().strip() + except (KeyboardInterrupt, EOFError): + raise Abort() from None + if value in ("y", "yes"): + rv = True + elif value in ("n", "no"): + rv = False + elif default is not None and value == "": + rv = default + else: + echo(_("Error: invalid input"), err=err) + continue + break + if abort and not rv: + raise Abort() + return rv + + +def get_terminal_size() -> os.terminal_size: + """Returns the current size of the terminal as tuple in the form + ``(width, height)`` in columns and rows. + + .. deprecated:: 8.0 + Will be removed in Click 8.1. Use + :func:`shutil.get_terminal_size` instead. + """ + import shutil + import warnings + + warnings.warn( + "'click.get_terminal_size()' is deprecated and will be removed" + " in Click 8.1. Use 'shutil.get_terminal_size()' instead.", + DeprecationWarning, + stacklevel=2, + ) + return shutil.get_terminal_size() + + +def echo_via_pager( + text_or_generator: t.Union[t.Iterable[str], t.Callable[[], t.Iterable[str]], str], + color: t.Optional[bool] = None, +) -> None: + """This function takes a text and shows it via an environment specific + pager on stdout. + + .. versionchanged:: 3.0 + Added the `color` flag. + + :param text_or_generator: the text to page, or alternatively, a + generator emitting the text to page. + :param color: controls if the pager supports ANSI colors or not. The + default is autodetection. + """ + color = resolve_color_default(color) + + if inspect.isgeneratorfunction(text_or_generator): + i = t.cast(t.Callable[[], t.Iterable[str]], text_or_generator)() + elif isinstance(text_or_generator, str): + i = [text_or_generator] + else: + i = iter(t.cast(t.Iterable[str], text_or_generator)) + + # convert every element of i to a text type if necessary + text_generator = (el if isinstance(el, str) else str(el) for el in i) + + from ._termui_impl import pager + + return pager(itertools.chain(text_generator, "\n"), color) + + +def progressbar( + iterable: t.Optional[t.Iterable[V]] = None, + length: t.Optional[int] = None, + label: t.Optional[str] = None, + show_eta: bool = True, + show_percent: t.Optional[bool] = None, + show_pos: bool = False, + item_show_func: t.Optional[t.Callable[[t.Optional[V]], t.Optional[str]]] = None, + fill_char: str = "#", + empty_char: str = "-", + bar_template: str = "%(label)s [%(bar)s] %(info)s", + info_sep: str = " ", + width: int = 36, + file: t.Optional[t.TextIO] = None, + color: t.Optional[bool] = None, + update_min_steps: int = 1, +) -> "ProgressBar[V]": + """This function creates an iterable context manager that can be used + to iterate over something while showing a progress bar. It will + either iterate over the `iterable` or `length` items (that are counted + up). While iteration happens, this function will print a rendered + progress bar to the given `file` (defaults to stdout) and will attempt + to calculate remaining time and more. By default, this progress bar + will not be rendered if the file is not a terminal. + + The context manager creates the progress bar. When the context + manager is entered the progress bar is already created. With every + iteration over the progress bar, the iterable passed to the bar is + advanced and the bar is updated. When the context manager exits, + a newline is printed and the progress bar is finalized on screen. + + Note: The progress bar is currently designed for use cases where the + total progress can be expected to take at least several seconds. + Because of this, the ProgressBar class object won't display + progress that is considered too fast, and progress where the time + between steps is less than a second. + + No printing must happen or the progress bar will be unintentionally + destroyed. + + Example usage:: + + with progressbar(items) as bar: + for item in bar: + do_something_with(item) + + Alternatively, if no iterable is specified, one can manually update the + progress bar through the `update()` method instead of directly + iterating over the progress bar. The update method accepts the number + of steps to increment the bar with:: + + with progressbar(length=chunks.total_bytes) as bar: + for chunk in chunks: + process_chunk(chunk) + bar.update(chunks.bytes) + + The ``update()`` method also takes an optional value specifying the + ``current_item`` at the new position. This is useful when used + together with ``item_show_func`` to customize the output for each + manual step:: + + with click.progressbar( + length=total_size, + label='Unzipping archive', + item_show_func=lambda a: a.filename + ) as bar: + for archive in zip_file: + archive.extract() + bar.update(archive.size, archive) + + :param iterable: an iterable to iterate over. If not provided the length + is required. + :param length: the number of items to iterate over. By default the + progressbar will attempt to ask the iterator about its + length, which might or might not work. If an iterable is + also provided this parameter can be used to override the + length. If an iterable is not provided the progress bar + will iterate over a range of that length. + :param label: the label to show next to the progress bar. + :param show_eta: enables or disables the estimated time display. This is + automatically disabled if the length cannot be + determined. + :param show_percent: enables or disables the percentage display. The + default is `True` if the iterable has a length or + `False` if not. + :param show_pos: enables or disables the absolute position display. The + default is `False`. + :param item_show_func: A function called with the current item which + can return a string to show next to the progress bar. If the + function returns ``None`` nothing is shown. The current item can + be ``None``, such as when entering and exiting the bar. + :param fill_char: the character to use to show the filled part of the + progress bar. + :param empty_char: the character to use to show the non-filled part of + the progress bar. + :param bar_template: the format string to use as template for the bar. + The parameters in it are ``label`` for the label, + ``bar`` for the progress bar and ``info`` for the + info section. + :param info_sep: the separator between multiple info items (eta etc.) + :param width: the width of the progress bar in characters, 0 means full + terminal width + :param file: The file to write to. If this is not a terminal then + only the label is printed. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are included anywhere in the progress bar output + which is not the case by default. + :param update_min_steps: Render only when this many updates have + completed. This allows tuning for very fast iterators. + + .. versionchanged:: 8.0 + Output is shown even if execution time is less than 0.5 seconds. + + .. versionchanged:: 8.0 + ``item_show_func`` shows the current item, not the previous one. + + .. versionchanged:: 8.0 + Labels are echoed if the output is not a TTY. Reverts a change + in 7.0 that removed all output. + + .. versionadded:: 8.0 + Added the ``update_min_steps`` parameter. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. Added the ``update`` method to + the object. + + .. versionadded:: 2.0 + """ + from ._termui_impl import ProgressBar + + color = resolve_color_default(color) + return ProgressBar( + iterable=iterable, + length=length, + show_eta=show_eta, + show_percent=show_percent, + show_pos=show_pos, + item_show_func=item_show_func, + fill_char=fill_char, + empty_char=empty_char, + bar_template=bar_template, + info_sep=info_sep, + file=file, + label=label, + width=width, + color=color, + update_min_steps=update_min_steps, + ) + + +def clear() -> None: + """Clears the terminal screen. This will have the effect of clearing + the whole visible space of the terminal and moving the cursor to the + top left. This does not do anything if not connected to a terminal. + + .. versionadded:: 2.0 + """ + if not isatty(sys.stdout): + return + if WIN: + os.system("cls") + else: + sys.stdout.write("\033[2J\033[1;1H") + + +def _interpret_color( + color: t.Union[int, t.Tuple[int, int, int], str], offset: int = 0 +) -> str: + if isinstance(color, int): + return f"{38 + offset};5;{color:d}" + + if isinstance(color, (tuple, list)): + r, g, b = color + return f"{38 + offset};2;{r:d};{g:d};{b:d}" + + return str(_ansi_colors[color] + offset) + + +def style( + text: t.Any, + fg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None, + bg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None, + bold: t.Optional[bool] = None, + dim: t.Optional[bool] = None, + underline: t.Optional[bool] = None, + overline: t.Optional[bool] = None, + italic: t.Optional[bool] = None, + blink: t.Optional[bool] = None, + reverse: t.Optional[bool] = None, + strikethrough: t.Optional[bool] = None, + reset: bool = True, +) -> str: + """Styles a text with ANSI styles and returns the new string. By + default the styling is self contained which means that at the end + of the string a reset code is issued. This can be prevented by + passing ``reset=False``. + + Examples:: + + click.echo(click.style('Hello World!', fg='green')) + click.echo(click.style('ATTENTION!', blink=True)) + click.echo(click.style('Some things', reverse=True, fg='cyan')) + click.echo(click.style('More colors', fg=(255, 12, 128), bg=117)) + + Supported color names: + + * ``black`` (might be a gray) + * ``red`` + * ``green`` + * ``yellow`` (might be an orange) + * ``blue`` + * ``magenta`` + * ``cyan`` + * ``white`` (might be light gray) + * ``bright_black`` + * ``bright_red`` + * ``bright_green`` + * ``bright_yellow`` + * ``bright_blue`` + * ``bright_magenta`` + * ``bright_cyan`` + * ``bright_white`` + * ``reset`` (reset the color code only) + + If the terminal supports it, color may also be specified as: + + - An integer in the interval [0, 255]. The terminal must support + 8-bit/256-color mode. + - An RGB tuple of three integers in [0, 255]. The terminal must + support 24-bit/true-color mode. + + See https://en.wikipedia.org/wiki/ANSI_color and + https://gist.github.com/XVilka/8346728 for more information. + + :param text: the string to style with ansi codes. + :param fg: if provided this will become the foreground color. + :param bg: if provided this will become the background color. + :param bold: if provided this will enable or disable bold mode. + :param dim: if provided this will enable or disable dim mode. This is + badly supported. + :param underline: if provided this will enable or disable underline. + :param overline: if provided this will enable or disable overline. + :param italic: if provided this will enable or disable italic. + :param blink: if provided this will enable or disable blinking. + :param reverse: if provided this will enable or disable inverse + rendering (foreground becomes background and the + other way round). + :param strikethrough: if provided this will enable or disable + striking through text. + :param reset: by default a reset-all code is added at the end of the + string which means that styles do not carry over. This + can be disabled to compose styles. + + .. versionchanged:: 8.0 + A non-string ``message`` is converted to a string. + + .. versionchanged:: 8.0 + Added support for 256 and RGB color codes. + + .. versionchanged:: 8.0 + Added the ``strikethrough``, ``italic``, and ``overline`` + parameters. + + .. versionchanged:: 7.0 + Added support for bright colors. + + .. versionadded:: 2.0 + """ + if not isinstance(text, str): + text = str(text) + + bits = [] + + if fg: + try: + bits.append(f"\033[{_interpret_color(fg)}m") + except KeyError: + raise TypeError(f"Unknown color {fg!r}") from None + + if bg: + try: + bits.append(f"\033[{_interpret_color(bg, 10)}m") + except KeyError: + raise TypeError(f"Unknown color {bg!r}") from None + + if bold is not None: + bits.append(f"\033[{1 if bold else 22}m") + if dim is not None: + bits.append(f"\033[{2 if dim else 22}m") + if underline is not None: + bits.append(f"\033[{4 if underline else 24}m") + if overline is not None: + bits.append(f"\033[{53 if overline else 55}m") + if italic is not None: + bits.append(f"\033[{3 if italic else 23}m") + if blink is not None: + bits.append(f"\033[{5 if blink else 25}m") + if reverse is not None: + bits.append(f"\033[{7 if reverse else 27}m") + if strikethrough is not None: + bits.append(f"\033[{9 if strikethrough else 29}m") + bits.append(text) + if reset: + bits.append(_ansi_reset_all) + return "".join(bits) + + +def unstyle(text: str) -> str: + """Removes ANSI styling information from a string. Usually it's not + necessary to use this function as Click's echo function will + automatically remove styling if necessary. + + .. versionadded:: 2.0 + + :param text: the text to remove style information from. + """ + return strip_ansi(text) + + +def secho( + message: t.Optional[t.Any] = None, + file: t.Optional[t.IO] = None, + nl: bool = True, + err: bool = False, + color: t.Optional[bool] = None, + **styles: t.Any, +) -> None: + """This function combines :func:`echo` and :func:`style` into one + call. As such the following two calls are the same:: + + click.secho('Hello World!', fg='green') + click.echo(click.style('Hello World!', fg='green')) + + All keyword arguments are forwarded to the underlying functions + depending on which one they go with. + + Non-string types will be converted to :class:`str`. However, + :class:`bytes` are passed directly to :meth:`echo` without applying + style. If you want to style bytes that represent text, call + :meth:`bytes.decode` first. + + .. versionchanged:: 8.0 + A non-string ``message`` is converted to a string. Bytes are + passed through without style applied. + + .. versionadded:: 2.0 + """ + if message is not None and not isinstance(message, (bytes, bytearray)): + message = style(message, **styles) + + return echo(message, file=file, nl=nl, err=err, color=color) + + +def edit( + text: t.Optional[t.AnyStr] = None, + editor: t.Optional[str] = None, + env: t.Optional[t.Mapping[str, str]] = None, + require_save: bool = True, + extension: str = ".txt", + filename: t.Optional[str] = None, +) -> t.Optional[t.AnyStr]: + r"""Edits the given text in the defined editor. If an editor is given + (should be the full path to the executable but the regular operating + system search path is used for finding the executable) it overrides + the detected editor. Optionally, some environment variables can be + used. If the editor is closed without changes, `None` is returned. In + case a file is edited directly the return value is always `None` and + `require_save` and `extension` are ignored. + + If the editor cannot be opened a :exc:`UsageError` is raised. + + Note for Windows: to simplify cross-platform usage, the newlines are + automatically converted from POSIX to Windows and vice versa. As such, + the message here will have ``\n`` as newline markers. + + :param text: the text to edit. + :param editor: optionally the editor to use. Defaults to automatic + detection. + :param env: environment variables to forward to the editor. + :param require_save: if this is true, then not saving in the editor + will make the return value become `None`. + :param extension: the extension to tell the editor about. This defaults + to `.txt` but changing this might change syntax + highlighting. + :param filename: if provided it will edit this file instead of the + provided text contents. It will not use a temporary + file as an indirection in that case. + """ + from ._termui_impl import Editor + + ed = Editor(editor=editor, env=env, require_save=require_save, extension=extension) + + if filename is None: + return ed.edit(text) + + ed.edit_file(filename) + return None + + +def launch(url: str, wait: bool = False, locate: bool = False) -> int: + """This function launches the given URL (or filename) in the default + viewer application for this file type. If this is an executable, it + might launch the executable in a new session. The return value is + the exit code of the launched application. Usually, ``0`` indicates + success. + + Examples:: + + click.launch('https://click.palletsprojects.com/') + click.launch('/my/downloaded/file', locate=True) + + .. versionadded:: 2.0 + + :param url: URL or filename of the thing to launch. + :param wait: Wait for the program to exit before returning. This + only works if the launched program blocks. In particular, + ``xdg-open`` on Linux does not block. + :param locate: if this is set to `True` then instead of launching the + application associated with the URL it will attempt to + launch a file manager with the file located. This + might have weird effects if the URL does not point to + the filesystem. + """ + from ._termui_impl import open_url + + return open_url(url, wait=wait, locate=locate) + + +# If this is provided, getchar() calls into this instead. This is used +# for unittesting purposes. +_getchar: t.Optional[t.Callable[[bool], str]] = None + + +def getchar(echo: bool = False) -> str: + """Fetches a single character from the terminal and returns it. This + will always return a unicode character and under certain rare + circumstances this might return more than one character. The + situations which more than one character is returned is when for + whatever reason multiple characters end up in the terminal buffer or + standard input was not actually a terminal. + + Note that this will always read from the terminal, even if something + is piped into the standard input. + + Note for Windows: in rare cases when typing non-ASCII characters, this + function might wait for a second character and then return both at once. + This is because certain Unicode characters look like special-key markers. + + .. versionadded:: 2.0 + + :param echo: if set to `True`, the character read will also show up on + the terminal. The default is to not show it. + """ + global _getchar + + if _getchar is None: + from ._termui_impl import getchar as f + + _getchar = f + + return _getchar(echo) + + +def raw_terminal() -> t.ContextManager[int]: + from ._termui_impl import raw_terminal as f + + return f() + + +def pause(info: t.Optional[str] = None, err: bool = False) -> None: + """This command stops execution and waits for the user to press any + key to continue. This is similar to the Windows batch "pause" + command. If the program is not run through a terminal, this command + will instead do nothing. + + .. versionadded:: 2.0 + + .. versionadded:: 4.0 + Added the `err` parameter. + + :param info: The message to print before pausing. Defaults to + ``"Press any key to continue..."``. + :param err: if set to message goes to ``stderr`` instead of + ``stdout``, the same as with echo. + """ + if not isatty(sys.stdin) or not isatty(sys.stdout): + return + + if info is None: + info = _("Press any key to continue...") + + try: + if info: + echo(info, nl=False, err=err) + try: + getchar() + except (KeyboardInterrupt, EOFError): + pass + finally: + if info: + echo(err=err) diff --git a/flask-app/venv/lib/python3.8/site-packages/click/testing.py b/flask-app/venv/lib/python3.8/site-packages/click/testing.py new file mode 100644 index 000000000..d19b850fc --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/testing.py @@ -0,0 +1,479 @@ +import contextlib +import io +import os +import shlex +import shutil +import sys +import tempfile +import typing as t +from types import TracebackType + +from . import formatting +from . import termui +from . import utils +from ._compat import _find_binary_reader + +if t.TYPE_CHECKING: + from .core import BaseCommand + + +class EchoingStdin: + def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None: + self._input = input + self._output = output + self._paused = False + + def __getattr__(self, x: str) -> t.Any: + return getattr(self._input, x) + + def _echo(self, rv: bytes) -> bytes: + if not self._paused: + self._output.write(rv) + + return rv + + def read(self, n: int = -1) -> bytes: + return self._echo(self._input.read(n)) + + def read1(self, n: int = -1) -> bytes: + return self._echo(self._input.read1(n)) # type: ignore + + def readline(self, n: int = -1) -> bytes: + return self._echo(self._input.readline(n)) + + def readlines(self) -> t.List[bytes]: + return [self._echo(x) for x in self._input.readlines()] + + def __iter__(self) -> t.Iterator[bytes]: + return iter(self._echo(x) for x in self._input) + + def __repr__(self) -> str: + return repr(self._input) + + +@contextlib.contextmanager +def _pause_echo(stream: t.Optional[EchoingStdin]) -> t.Iterator[None]: + if stream is None: + yield + else: + stream._paused = True + yield + stream._paused = False + + +class _NamedTextIOWrapper(io.TextIOWrapper): + def __init__( + self, buffer: t.BinaryIO, name: str, mode: str, **kwargs: t.Any + ) -> None: + super().__init__(buffer, **kwargs) + self._name = name + self._mode = mode + + @property + def name(self) -> str: + return self._name + + @property + def mode(self) -> str: + return self._mode + + +def make_input_stream( + input: t.Optional[t.Union[str, bytes, t.IO]], charset: str +) -> t.BinaryIO: + # Is already an input stream. + if hasattr(input, "read"): + rv = _find_binary_reader(t.cast(t.IO, input)) + + if rv is not None: + return rv + + raise TypeError("Could not find binary reader for input stream.") + + if input is None: + input = b"" + elif isinstance(input, str): + input = input.encode(charset) + + return io.BytesIO(t.cast(bytes, input)) + + +class Result: + """Holds the captured result of an invoked CLI script.""" + + def __init__( + self, + runner: "CliRunner", + stdout_bytes: bytes, + stderr_bytes: t.Optional[bytes], + return_value: t.Any, + exit_code: int, + exception: t.Optional[BaseException], + exc_info: t.Optional[ + t.Tuple[t.Type[BaseException], BaseException, TracebackType] + ] = None, + ): + #: The runner that created the result + self.runner = runner + #: The standard output as bytes. + self.stdout_bytes = stdout_bytes + #: The standard error as bytes, or None if not available + self.stderr_bytes = stderr_bytes + #: The value returned from the invoked command. + #: + #: .. versionadded:: 8.0 + self.return_value = return_value + #: The exit code as integer. + self.exit_code = exit_code + #: The exception that happened if one did. + self.exception = exception + #: The traceback + self.exc_info = exc_info + + @property + def output(self) -> str: + """The (standard) output as unicode string.""" + return self.stdout + + @property + def stdout(self) -> str: + """The standard output as unicode string.""" + return self.stdout_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + @property + def stderr(self) -> str: + """The standard error as unicode string.""" + if self.stderr_bytes is None: + raise ValueError("stderr not separately captured") + return self.stderr_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + def __repr__(self) -> str: + exc_str = repr(self.exception) if self.exception else "okay" + return f"<{type(self).__name__} {exc_str}>" + + +class CliRunner: + """The CLI runner provides functionality to invoke a Click command line + script for unittesting purposes in a isolated environment. This only + works in single-threaded systems without any concurrency as it changes the + global interpreter state. + + :param charset: the character set for the input and output data. + :param env: a dictionary with environment variables for overriding. + :param echo_stdin: if this is set to `True`, then reading from stdin writes + to stdout. This is useful for showing examples in + some circumstances. Note that regular prompts + will automatically echo the input. + :param mix_stderr: if this is set to `False`, then stdout and stderr are + preserved as independent streams. This is useful for + Unix-philosophy apps that have predictable stdout and + noisy stderr, such that each may be measured + independently + """ + + def __init__( + self, + charset: str = "utf-8", + env: t.Optional[t.Mapping[str, t.Optional[str]]] = None, + echo_stdin: bool = False, + mix_stderr: bool = True, + ) -> None: + self.charset = charset + self.env = env or {} + self.echo_stdin = echo_stdin + self.mix_stderr = mix_stderr + + def get_default_prog_name(self, cli: "BaseCommand") -> str: + """Given a command object it will return the default program name + for it. The default is the `name` attribute or ``"root"`` if not + set. + """ + return cli.name or "root" + + def make_env( + self, overrides: t.Optional[t.Mapping[str, t.Optional[str]]] = None + ) -> t.Mapping[str, t.Optional[str]]: + """Returns the environment overrides for invoking a script.""" + rv = dict(self.env) + if overrides: + rv.update(overrides) + return rv + + @contextlib.contextmanager + def isolation( + self, + input: t.Optional[t.Union[str, bytes, t.IO]] = None, + env: t.Optional[t.Mapping[str, t.Optional[str]]] = None, + color: bool = False, + ) -> t.Iterator[t.Tuple[io.BytesIO, t.Optional[io.BytesIO]]]: + """A context manager that sets up the isolation for invoking of a + command line tool. This sets up stdin with the given input data + and `os.environ` with the overrides from the given dictionary. + This also rebinds some internals in Click to be mocked (like the + prompt functionality). + + This is automatically done in the :meth:`invoke` method. + + :param input: the input stream to put into sys.stdin. + :param env: the environment overrides as dictionary. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + + .. versionchanged:: 8.0 + ``stderr`` is opened with ``errors="backslashreplace"`` + instead of the default ``"strict"``. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + """ + bytes_input = make_input_stream(input, self.charset) + echo_input = None + + old_stdin = sys.stdin + old_stdout = sys.stdout + old_stderr = sys.stderr + old_forced_width = formatting.FORCED_WIDTH + formatting.FORCED_WIDTH = 80 + + env = self.make_env(env) + + bytes_output = io.BytesIO() + + if self.echo_stdin: + bytes_input = echo_input = t.cast( + t.BinaryIO, EchoingStdin(bytes_input, bytes_output) + ) + + sys.stdin = text_input = _NamedTextIOWrapper( + bytes_input, encoding=self.charset, name="", mode="r" + ) + + if self.echo_stdin: + # Force unbuffered reads, otherwise TextIOWrapper reads a + # large chunk which is echoed early. + text_input._CHUNK_SIZE = 1 # type: ignore + + sys.stdout = _NamedTextIOWrapper( + bytes_output, encoding=self.charset, name="", mode="w" + ) + + bytes_error = None + if self.mix_stderr: + sys.stderr = sys.stdout + else: + bytes_error = io.BytesIO() + sys.stderr = _NamedTextIOWrapper( + bytes_error, + encoding=self.charset, + name="", + mode="w", + errors="backslashreplace", + ) + + @_pause_echo(echo_input) # type: ignore + def visible_input(prompt: t.Optional[str] = None) -> str: + sys.stdout.write(prompt or "") + val = text_input.readline().rstrip("\r\n") + sys.stdout.write(f"{val}\n") + sys.stdout.flush() + return val + + @_pause_echo(echo_input) # type: ignore + def hidden_input(prompt: t.Optional[str] = None) -> str: + sys.stdout.write(f"{prompt or ''}\n") + sys.stdout.flush() + return text_input.readline().rstrip("\r\n") + + @_pause_echo(echo_input) # type: ignore + def _getchar(echo: bool) -> str: + char = sys.stdin.read(1) + + if echo: + sys.stdout.write(char) + + sys.stdout.flush() + return char + + default_color = color + + def should_strip_ansi( + stream: t.Optional[t.IO] = None, color: t.Optional[bool] = None + ) -> bool: + if color is None: + return not default_color + return not color + + old_visible_prompt_func = termui.visible_prompt_func + old_hidden_prompt_func = termui.hidden_prompt_func + old__getchar_func = termui._getchar + old_should_strip_ansi = utils.should_strip_ansi # type: ignore + termui.visible_prompt_func = visible_input + termui.hidden_prompt_func = hidden_input + termui._getchar = _getchar + utils.should_strip_ansi = should_strip_ansi # type: ignore + + old_env = {} + try: + for key, value in env.items(): + old_env[key] = os.environ.get(key) + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + yield (bytes_output, bytes_error) + finally: + for key, value in old_env.items(): + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + sys.stdout = old_stdout + sys.stderr = old_stderr + sys.stdin = old_stdin + termui.visible_prompt_func = old_visible_prompt_func + termui.hidden_prompt_func = old_hidden_prompt_func + termui._getchar = old__getchar_func + utils.should_strip_ansi = old_should_strip_ansi # type: ignore + formatting.FORCED_WIDTH = old_forced_width + + def invoke( + self, + cli: "BaseCommand", + args: t.Optional[t.Union[str, t.Sequence[str]]] = None, + input: t.Optional[t.Union[str, bytes, t.IO]] = None, + env: t.Optional[t.Mapping[str, t.Optional[str]]] = None, + catch_exceptions: bool = True, + color: bool = False, + **extra: t.Any, + ) -> Result: + """Invokes a command in an isolated environment. The arguments are + forwarded directly to the command line script, the `extra` keyword + arguments are passed to the :meth:`~clickpkg.Command.main` function of + the command. + + This returns a :class:`Result` object. + + :param cli: the command to invoke + :param args: the arguments to invoke. It may be given as an iterable + or a string. When given as string it will be interpreted + as a Unix shell command. More details at + :func:`shlex.split`. + :param input: the input data for `sys.stdin`. + :param env: the environment overrides. + :param catch_exceptions: Whether to catch any other exceptions than + ``SystemExit``. + :param extra: the keyword arguments to pass to :meth:`main`. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + + .. versionchanged:: 8.0 + The result object has the ``return_value`` attribute with + the value returned from the invoked command. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + + .. versionchanged:: 3.0 + Added the ``catch_exceptions`` parameter. + + .. versionchanged:: 3.0 + The result object has the ``exc_info`` attribute with the + traceback if available. + """ + exc_info = None + with self.isolation(input=input, env=env, color=color) as outstreams: + return_value = None + exception: t.Optional[BaseException] = None + exit_code = 0 + + if isinstance(args, str): + args = shlex.split(args) + + try: + prog_name = extra.pop("prog_name") + except KeyError: + prog_name = self.get_default_prog_name(cli) + + try: + return_value = cli.main(args=args or (), prog_name=prog_name, **extra) + except SystemExit as e: + exc_info = sys.exc_info() + e_code = t.cast(t.Optional[t.Union[int, t.Any]], e.code) + + if e_code is None: + e_code = 0 + + if e_code != 0: + exception = e + + if not isinstance(e_code, int): + sys.stdout.write(str(e_code)) + sys.stdout.write("\n") + e_code = 1 + + exit_code = e_code + + except Exception as e: + if not catch_exceptions: + raise + exception = e + exit_code = 1 + exc_info = sys.exc_info() + finally: + sys.stdout.flush() + stdout = outstreams[0].getvalue() + if self.mix_stderr: + stderr = None + else: + stderr = outstreams[1].getvalue() # type: ignore + + return Result( + runner=self, + stdout_bytes=stdout, + stderr_bytes=stderr, + return_value=return_value, + exit_code=exit_code, + exception=exception, + exc_info=exc_info, # type: ignore + ) + + @contextlib.contextmanager + def isolated_filesystem( + self, temp_dir: t.Optional[t.Union[str, os.PathLike]] = None + ) -> t.Iterator[str]: + """A context manager that creates a temporary directory and + changes the current working directory to it. This isolates tests + that affect the contents of the CWD to prevent them from + interfering with each other. + + :param temp_dir: Create the temporary directory under this + directory. If given, the created directory is not removed + when exiting. + + .. versionchanged:: 8.0 + Added the ``temp_dir`` parameter. + """ + cwd = os.getcwd() + t = tempfile.mkdtemp(dir=temp_dir) + os.chdir(t) + + try: + yield t + finally: + os.chdir(cwd) + + if temp_dir is None: + try: + shutil.rmtree(t) + except OSError: # noqa: B014 + pass diff --git a/flask-app/venv/lib/python3.8/site-packages/click/types.py b/flask-app/venv/lib/python3.8/site-packages/click/types.py new file mode 100644 index 000000000..103d21829 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/types.py @@ -0,0 +1,1052 @@ +import os +import stat +import typing as t +from datetime import datetime +from gettext import gettext as _ +from gettext import ngettext + +from ._compat import _get_argv_encoding +from ._compat import get_filesystem_encoding +from ._compat import open_stream +from .exceptions import BadParameter +from .utils import LazyFile +from .utils import safecall + +if t.TYPE_CHECKING: + import typing_extensions as te + from .core import Context + from .core import Parameter + from .shell_completion import CompletionItem + + +class ParamType: + """Represents the type of a parameter. Validates and converts values + from the command line or Python into the correct type. + + To implement a custom type, subclass and implement at least the + following: + + - The :attr:`name` class attribute must be set. + - Calling an instance of the type with ``None`` must return + ``None``. This is already implemented by default. + - :meth:`convert` must convert string values to the correct type. + - :meth:`convert` must accept values that are already the correct + type. + - It must be able to convert a value if the ``ctx`` and ``param`` + arguments are ``None``. This can occur when converting prompt + input. + """ + + is_composite: t.ClassVar[bool] = False + arity: t.ClassVar[int] = 1 + + #: the descriptive name of this type + name: str + + #: if a list of this type is expected and the value is pulled from a + #: string environment variable, this is what splits it up. `None` + #: means any whitespace. For all parameters the general rule is that + #: whitespace splits them up. The exception are paths and files which + #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on + #: Windows). + envvar_list_splitter: t.ClassVar[t.Optional[str]] = None + + def to_info_dict(self) -> t.Dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. + + Use :meth:`click.Context.to_info_dict` to traverse the entire + CLI structure. + + .. versionadded:: 8.0 + """ + # The class name without the "ParamType" suffix. + param_type = type(self).__name__.partition("ParamType")[0] + param_type = param_type.partition("ParameterType")[0] + return {"param_type": param_type, "name": self.name} + + def __call__( + self, + value: t.Any, + param: t.Optional["Parameter"] = None, + ctx: t.Optional["Context"] = None, + ) -> t.Any: + if value is not None: + return self.convert(value, param, ctx) + + def get_metavar(self, param: "Parameter") -> t.Optional[str]: + """Returns the metavar default for this param if it provides one.""" + + def get_missing_message(self, param: "Parameter") -> t.Optional[str]: + """Optionally might return extra information about a missing + parameter. + + .. versionadded:: 2.0 + """ + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + """Convert the value to the correct type. This is not called if + the value is ``None`` (the missing value). + + This must accept string values from the command line, as well as + values that are already the correct type. It may also convert + other compatible types. + + The ``param`` and ``ctx`` arguments may be ``None`` in certain + situations, such as when converting prompt input. + + If the value cannot be converted, call :meth:`fail` with a + descriptive message. + + :param value: The value to convert. + :param param: The parameter that is using this type to convert + its value. May be ``None``. + :param ctx: The current context that arrived at this value. May + be ``None``. + """ + return value + + def split_envvar_value(self, rv: str) -> t.Sequence[str]: + """Given a value from an environment variable this splits it up + into small chunks depending on the defined envvar list splitter. + + If the splitter is set to `None`, which means that whitespace splits, + then leading and trailing whitespace is ignored. Otherwise, leading + and trailing splitters usually lead to empty items being included. + """ + return (rv or "").split(self.envvar_list_splitter) + + def fail( + self, + message: str, + param: t.Optional["Parameter"] = None, + ctx: t.Optional["Context"] = None, + ) -> "t.NoReturn": + """Helper method to fail with an invalid value message.""" + raise BadParameter(message, ctx=ctx, param=param) + + def shell_complete( + self, ctx: "Context", param: "Parameter", incomplete: str + ) -> t.List["CompletionItem"]: + """Return a list of + :class:`~click.shell_completion.CompletionItem` objects for the + incomplete value. Most types do not provide completions, but + some do, and this allows custom types to provide custom + completions as well. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + return [] + + +class CompositeParamType(ParamType): + is_composite = True + + @property + def arity(self) -> int: # type: ignore + raise NotImplementedError() + + +class FuncParamType(ParamType): + def __init__(self, func: t.Callable[[t.Any], t.Any]) -> None: + self.name = func.__name__ + self.func = func + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["func"] = self.func + return info_dict + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + try: + return self.func(value) + except ValueError: + try: + value = str(value) + except UnicodeError: + value = value.decode("utf-8", "replace") + + self.fail(value, param, ctx) + + +class UnprocessedParamType(ParamType): + name = "text" + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + return value + + def __repr__(self) -> str: + return "UNPROCESSED" + + +class StringParamType(ParamType): + name = "text" + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + if isinstance(value, bytes): + enc = _get_argv_encoding() + try: + value = value.decode(enc) + except UnicodeError: + fs_enc = get_filesystem_encoding() + if fs_enc != enc: + try: + value = value.decode(fs_enc) + except UnicodeError: + value = value.decode("utf-8", "replace") + else: + value = value.decode("utf-8", "replace") + return value + return str(value) + + def __repr__(self) -> str: + return "STRING" + + +class Choice(ParamType): + """The choice type allows a value to be checked against a fixed set + of supported values. All of these values have to be strings. + + You should only pass a list or tuple of choices. Other iterables + (like generators) may lead to surprising results. + + The resulting value will always be one of the originally passed choices + regardless of ``case_sensitive`` or any ``ctx.token_normalize_func`` + being specified. + + See :ref:`choice-opts` for an example. + + :param case_sensitive: Set to false to make choices case + insensitive. Defaults to true. + """ + + name = "choice" + + def __init__(self, choices: t.Sequence[str], case_sensitive: bool = True) -> None: + self.choices = choices + self.case_sensitive = case_sensitive + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["choices"] = self.choices + info_dict["case_sensitive"] = self.case_sensitive + return info_dict + + def get_metavar(self, param: "Parameter") -> str: + choices_str = "|".join(self.choices) + + # Use curly braces to indicate a required argument. + if param.required and param.param_type_name == "argument": + return f"{{{choices_str}}}" + + # Use square braces to indicate an option or optional argument. + return f"[{choices_str}]" + + def get_missing_message(self, param: "Parameter") -> str: + return _("Choose from:\n\t{choices}").format(choices=",\n\t".join(self.choices)) + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + # Match through normalization and case sensitivity + # first do token_normalize_func, then lowercase + # preserve original `value` to produce an accurate message in + # `self.fail` + normed_value = value + normed_choices = {choice: choice for choice in self.choices} + + if ctx is not None and ctx.token_normalize_func is not None: + normed_value = ctx.token_normalize_func(value) + normed_choices = { + ctx.token_normalize_func(normed_choice): original + for normed_choice, original in normed_choices.items() + } + + if not self.case_sensitive: + normed_value = normed_value.casefold() + normed_choices = { + normed_choice.casefold(): original + for normed_choice, original in normed_choices.items() + } + + if normed_value in normed_choices: + return normed_choices[normed_value] + + choices_str = ", ".join(map(repr, self.choices)) + self.fail( + ngettext( + "{value!r} is not {choice}.", + "{value!r} is not one of {choices}.", + len(self.choices), + ).format(value=value, choice=choices_str, choices=choices_str), + param, + ctx, + ) + + def __repr__(self) -> str: + return f"Choice({list(self.choices)})" + + def shell_complete( + self, ctx: "Context", param: "Parameter", incomplete: str + ) -> t.List["CompletionItem"]: + """Complete choices that start with the incomplete value. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + str_choices = map(str, self.choices) + + if self.case_sensitive: + matched = (c for c in str_choices if c.startswith(incomplete)) + else: + incomplete = incomplete.lower() + matched = (c for c in str_choices if c.lower().startswith(incomplete)) + + return [CompletionItem(c) for c in matched] + + +class DateTime(ParamType): + """The DateTime type converts date strings into `datetime` objects. + + The format strings which are checked are configurable, but default to some + common (non-timezone aware) ISO 8601 formats. + + When specifying *DateTime* formats, you should only pass a list or a tuple. + Other iterables, like generators, may lead to surprising results. + + The format strings are processed using ``datetime.strptime``, and this + consequently defines the format strings which are allowed. + + Parsing is tried using each format, in order, and the first format which + parses successfully is used. + + :param formats: A list or tuple of date format strings, in the order in + which they should be tried. Defaults to + ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``, + ``'%Y-%m-%d %H:%M:%S'``. + """ + + name = "datetime" + + def __init__(self, formats: t.Optional[t.Sequence[str]] = None): + self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"] + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["formats"] = self.formats + return info_dict + + def get_metavar(self, param: "Parameter") -> str: + return f"[{'|'.join(self.formats)}]" + + def _try_to_convert_date(self, value: t.Any, format: str) -> t.Optional[datetime]: + try: + return datetime.strptime(value, format) + except ValueError: + return None + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + if isinstance(value, datetime): + return value + + for format in self.formats: + converted = self._try_to_convert_date(value, format) + + if converted is not None: + return converted + + formats_str = ", ".join(map(repr, self.formats)) + self.fail( + ngettext( + "{value!r} does not match the format {format}.", + "{value!r} does not match the formats {formats}.", + len(self.formats), + ).format(value=value, format=formats_str, formats=formats_str), + param, + ctx, + ) + + def __repr__(self) -> str: + return "DateTime" + + +class _NumberParamTypeBase(ParamType): + _number_class: t.ClassVar[t.Type] + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + try: + return self._number_class(value) + except ValueError: + self.fail( + _("{value!r} is not a valid {number_type}.").format( + value=value, number_type=self.name + ), + param, + ctx, + ) + + +class _NumberRangeBase(_NumberParamTypeBase): + def __init__( + self, + min: t.Optional[float] = None, + max: t.Optional[float] = None, + min_open: bool = False, + max_open: bool = False, + clamp: bool = False, + ) -> None: + self.min = min + self.max = max + self.min_open = min_open + self.max_open = max_open + self.clamp = clamp + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update( + min=self.min, + max=self.max, + min_open=self.min_open, + max_open=self.max_open, + clamp=self.clamp, + ) + return info_dict + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + import operator + + rv = super().convert(value, param, ctx) + lt_min: bool = self.min is not None and ( + operator.le if self.min_open else operator.lt + )(rv, self.min) + gt_max: bool = self.max is not None and ( + operator.ge if self.max_open else operator.gt + )(rv, self.max) + + if self.clamp: + if lt_min: + return self._clamp(self.min, 1, self.min_open) # type: ignore + + if gt_max: + return self._clamp(self.max, -1, self.max_open) # type: ignore + + if lt_min or gt_max: + self.fail( + _("{value} is not in the range {range}.").format( + value=rv, range=self._describe_range() + ), + param, + ctx, + ) + + return rv + + def _clamp(self, bound: float, dir: "te.Literal[1, -1]", open: bool) -> float: + """Find the valid value to clamp to bound in the given + direction. + + :param bound: The boundary value. + :param dir: 1 or -1 indicating the direction to move. + :param open: If true, the range does not include the bound. + """ + raise NotImplementedError + + def _describe_range(self) -> str: + """Describe the range for use in help text.""" + if self.min is None: + op = "<" if self.max_open else "<=" + return f"x{op}{self.max}" + + if self.max is None: + op = ">" if self.min_open else ">=" + return f"x{op}{self.min}" + + lop = "<" if self.min_open else "<=" + rop = "<" if self.max_open else "<=" + return f"{self.min}{lop}x{rop}{self.max}" + + def __repr__(self) -> str: + clamp = " clamped" if self.clamp else "" + return f"<{type(self).__name__} {self._describe_range()}{clamp}>" + + +class IntParamType(_NumberParamTypeBase): + name = "integer" + _number_class = int + + def __repr__(self) -> str: + return "INT" + + +class IntRange(_NumberRangeBase, IntParamType): + """Restrict an :data:`click.INT` value to a range of accepted + values. See :ref:`ranges`. + + If ``min`` or ``max`` are not passed, any value is accepted in that + direction. If ``min_open`` or ``max_open`` are enabled, the + corresponding boundary is not included in the range. + + If ``clamp`` is enabled, a value outside the range is clamped to the + boundary instead of failing. + + .. versionchanged:: 8.0 + Added the ``min_open`` and ``max_open`` parameters. + """ + + name = "integer range" + + def _clamp( # type: ignore + self, bound: int, dir: "te.Literal[1, -1]", open: bool + ) -> int: + if not open: + return bound + + return bound + dir + + +class FloatParamType(_NumberParamTypeBase): + name = "float" + _number_class = float + + def __repr__(self) -> str: + return "FLOAT" + + +class FloatRange(_NumberRangeBase, FloatParamType): + """Restrict a :data:`click.FLOAT` value to a range of accepted + values. See :ref:`ranges`. + + If ``min`` or ``max`` are not passed, any value is accepted in that + direction. If ``min_open`` or ``max_open`` are enabled, the + corresponding boundary is not included in the range. + + If ``clamp`` is enabled, a value outside the range is clamped to the + boundary instead of failing. This is not supported if either + boundary is marked ``open``. + + .. versionchanged:: 8.0 + Added the ``min_open`` and ``max_open`` parameters. + """ + + name = "float range" + + def __init__( + self, + min: t.Optional[float] = None, + max: t.Optional[float] = None, + min_open: bool = False, + max_open: bool = False, + clamp: bool = False, + ) -> None: + super().__init__( + min=min, max=max, min_open=min_open, max_open=max_open, clamp=clamp + ) + + if (min_open or max_open) and clamp: + raise TypeError("Clamping is not supported for open bounds.") + + def _clamp(self, bound: float, dir: "te.Literal[1, -1]", open: bool) -> float: + if not open: + return bound + + # Could use Python 3.9's math.nextafter here, but clamping an + # open float range doesn't seem to be particularly useful. It's + # left up to the user to write a callback to do it if needed. + raise RuntimeError("Clamping is not supported for open bounds.") + + +class BoolParamType(ParamType): + name = "boolean" + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + if value in {False, True}: + return bool(value) + + norm = value.strip().lower() + + if norm in {"1", "true", "t", "yes", "y", "on"}: + return True + + if norm in {"0", "false", "f", "no", "n", "off"}: + return False + + self.fail( + _("{value!r} is not a valid boolean.").format(value=value), param, ctx + ) + + def __repr__(self) -> str: + return "BOOL" + + +class UUIDParameterType(ParamType): + name = "uuid" + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + import uuid + + if isinstance(value, uuid.UUID): + return value + + value = value.strip() + + try: + return uuid.UUID(value) + except ValueError: + self.fail( + _("{value!r} is not a valid UUID.").format(value=value), param, ctx + ) + + def __repr__(self) -> str: + return "UUID" + + +class File(ParamType): + """Declares a parameter to be a file for reading or writing. The file + is automatically closed once the context tears down (after the command + finished working). + + Files can be opened for reading or writing. The special value ``-`` + indicates stdin or stdout depending on the mode. + + By default, the file is opened for reading text data, but it can also be + opened in binary mode or for writing. The encoding parameter can be used + to force a specific encoding. + + The `lazy` flag controls if the file should be opened immediately or upon + first IO. The default is to be non-lazy for standard input and output + streams as well as files opened for reading, `lazy` otherwise. When opening a + file lazily for reading, it is still opened temporarily for validation, but + will not be held open until first IO. lazy is mainly useful when opening + for writing to avoid creating the file until it is needed. + + Starting with Click 2.0, files can also be opened atomically in which + case all writes go into a separate file in the same folder and upon + completion the file will be moved over to the original location. This + is useful if a file regularly read by other users is modified. + + See :ref:`file-args` for more information. + """ + + name = "filename" + envvar_list_splitter = os.path.pathsep + + def __init__( + self, + mode: str = "r", + encoding: t.Optional[str] = None, + errors: t.Optional[str] = "strict", + lazy: t.Optional[bool] = None, + atomic: bool = False, + ) -> None: + self.mode = mode + self.encoding = encoding + self.errors = errors + self.lazy = lazy + self.atomic = atomic + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update(mode=self.mode, encoding=self.encoding) + return info_dict + + def resolve_lazy_flag(self, value: t.Any) -> bool: + if self.lazy is not None: + return self.lazy + if value == "-": + return False + elif "w" in self.mode: + return True + return False + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + try: + if hasattr(value, "read") or hasattr(value, "write"): + return value + + lazy = self.resolve_lazy_flag(value) + + if lazy: + f: t.IO = t.cast( + t.IO, + LazyFile( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ), + ) + + if ctx is not None: + ctx.call_on_close(f.close_intelligently) # type: ignore + + return f + + f, should_close = open_stream( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + + # If a context is provided, we automatically close the file + # at the end of the context execution (or flush out). If a + # context does not exist, it's the caller's responsibility to + # properly close the file. This for instance happens when the + # type is used with prompts. + if ctx is not None: + if should_close: + ctx.call_on_close(safecall(f.close)) + else: + ctx.call_on_close(safecall(f.flush)) + + return f + except OSError as e: # noqa: B014 + self.fail(f"{os.fsdecode(value)!r}: {e.strerror}", param, ctx) + + def shell_complete( + self, ctx: "Context", param: "Parameter", incomplete: str + ) -> t.List["CompletionItem"]: + """Return a special completion marker that tells the completion + system to use the shell to provide file path completions. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + return [CompletionItem(incomplete, type="file")] + + +class Path(ParamType): + """The path type is similar to the :class:`File` type but it performs + different checks. First of all, instead of returning an open file + handle it returns just the filename. Secondly, it can perform various + basic checks about what the file or directory should be. + + :param exists: if set to true, the file or directory needs to exist for + this value to be valid. If this is not required and a + file does indeed not exist, then all further checks are + silently skipped. + :param file_okay: controls if a file is a possible value. + :param dir_okay: controls if a directory is a possible value. + :param writable: if true, a writable check is performed. + :param readable: if true, a readable check is performed. + :param resolve_path: if this is true, then the path is fully resolved + before the value is passed onwards. This means + that it's absolute and symlinks are resolved. It + will not expand a tilde-prefix, as this is + supposed to be done by the shell only. + :param allow_dash: If this is set to `True`, a single dash to indicate + standard streams is permitted. + :param path_type: Convert the incoming path value to this type. If + ``None``, keep Python's default, which is ``str``. Useful to + convert to :class:`pathlib.Path`. + + .. versionchanged:: 8.0 + Allow passing ``type=pathlib.Path``. + + .. versionchanged:: 6.0 + Added the ``allow_dash`` parameter. + """ + + envvar_list_splitter = os.path.pathsep + + def __init__( + self, + exists: bool = False, + file_okay: bool = True, + dir_okay: bool = True, + writable: bool = False, + readable: bool = True, + resolve_path: bool = False, + allow_dash: bool = False, + path_type: t.Optional[t.Type] = None, + ): + self.exists = exists + self.file_okay = file_okay + self.dir_okay = dir_okay + self.writable = writable + self.readable = readable + self.resolve_path = resolve_path + self.allow_dash = allow_dash + self.type = path_type + + if self.file_okay and not self.dir_okay: + self.name = _("file") + elif self.dir_okay and not self.file_okay: + self.name = _("directory") + else: + self.name = _("path") + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update( + exists=self.exists, + file_okay=self.file_okay, + dir_okay=self.dir_okay, + writable=self.writable, + readable=self.readable, + allow_dash=self.allow_dash, + ) + return info_dict + + def coerce_path_result(self, rv: t.Any) -> t.Any: + if self.type is not None and not isinstance(rv, self.type): + if self.type is str: + rv = os.fsdecode(rv) + elif self.type is bytes: + rv = os.fsencode(rv) + else: + rv = self.type(rv) + + return rv + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + rv = value + + is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-") + + if not is_dash: + if self.resolve_path: + # os.path.realpath doesn't resolve symlinks on Windows + # until Python 3.8. Use pathlib for now. + import pathlib + + rv = os.fsdecode(pathlib.Path(rv).resolve()) + + try: + st = os.stat(rv) + except OSError: + if not self.exists: + return self.coerce_path_result(rv) + self.fail( + _("{name} {filename!r} does not exist.").format( + name=self.name.title(), filename=os.fsdecode(value) + ), + param, + ctx, + ) + + if not self.file_okay and stat.S_ISREG(st.st_mode): + self.fail( + _("{name} {filename!r} is a file.").format( + name=self.name.title(), filename=os.fsdecode(value) + ), + param, + ctx, + ) + if not self.dir_okay and stat.S_ISDIR(st.st_mode): + self.fail( + _("{name} {filename!r} is a directory.").format( + name=self.name.title(), filename=os.fsdecode(value) + ), + param, + ctx, + ) + if self.writable and not os.access(rv, os.W_OK): + self.fail( + _("{name} {filename!r} is not writable.").format( + name=self.name.title(), filename=os.fsdecode(value) + ), + param, + ctx, + ) + if self.readable and not os.access(rv, os.R_OK): + self.fail( + _("{name} {filename!r} is not readable.").format( + name=self.name.title(), filename=os.fsdecode(value) + ), + param, + ctx, + ) + + return self.coerce_path_result(rv) + + def shell_complete( + self, ctx: "Context", param: "Parameter", incomplete: str + ) -> t.List["CompletionItem"]: + """Return a special completion marker that tells the completion + system to use the shell to provide path completions for only + directories or any paths. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + type = "dir" if self.dir_okay and not self.file_okay else "file" + return [CompletionItem(incomplete, type=type)] + + +class Tuple(CompositeParamType): + """The default behavior of Click is to apply a type on a value directly. + This works well in most cases, except for when `nargs` is set to a fixed + count and different types should be used for different items. In this + case the :class:`Tuple` type can be used. This type can only be used + if `nargs` is set to a fixed number. + + For more information see :ref:`tuple-type`. + + This can be selected by using a Python tuple literal as a type. + + :param types: a list of types that should be used for the tuple items. + """ + + def __init__(self, types: t.Sequence[t.Union[t.Type, ParamType]]) -> None: + self.types = [convert_type(ty) for ty in types] + + def to_info_dict(self) -> t.Dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["types"] = [t.to_info_dict() for t in self.types] + return info_dict + + @property + def name(self) -> str: # type: ignore + return f"<{' '.join(ty.name for ty in self.types)}>" + + @property + def arity(self) -> int: # type: ignore + return len(self.types) + + def convert( + self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"] + ) -> t.Any: + len_type = len(self.types) + len_value = len(value) + + if len_value != len_type: + self.fail( + ngettext( + "{len_type} values are required, but {len_value} was given.", + "{len_type} values are required, but {len_value} were given.", + len_value, + ).format(len_type=len_type, len_value=len_value), + param=param, + ctx=ctx, + ) + + return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value)) + + +def convert_type(ty: t.Optional[t.Any], default: t.Optional[t.Any] = None) -> ParamType: + """Find the most appropriate :class:`ParamType` for the given Python + type. If the type isn't provided, it can be inferred from a default + value. + """ + guessed_type = False + + if ty is None and default is not None: + if isinstance(default, (tuple, list)): + # If the default is empty, ty will remain None and will + # return STRING. + if default: + item = default[0] + + # A tuple of tuples needs to detect the inner types. + # Can't call convert recursively because that would + # incorrectly unwind the tuple to a single type. + if isinstance(item, (tuple, list)): + ty = tuple(map(type, item)) + else: + ty = type(item) + else: + ty = type(default) + + guessed_type = True + + if isinstance(ty, tuple): + return Tuple(ty) + + if isinstance(ty, ParamType): + return ty + + if ty is str or ty is None: + return STRING + + if ty is int: + return INT + + if ty is float: + return FLOAT + + if ty is bool: + return BOOL + + if guessed_type: + return STRING + + if __debug__: + try: + if issubclass(ty, ParamType): + raise AssertionError( + f"Attempted to use an uninstantiated parameter type ({ty})." + ) + except TypeError: + # ty is an instance (correct), so issubclass fails. + pass + + return FuncParamType(ty) + + +#: A dummy parameter type that just does nothing. From a user's +#: perspective this appears to just be the same as `STRING` but +#: internally no string conversion takes place if the input was bytes. +#: This is usually useful when working with file paths as they can +#: appear in bytes and unicode. +#: +#: For path related uses the :class:`Path` type is a better choice but +#: there are situations where an unprocessed type is useful which is why +#: it is is provided. +#: +#: .. versionadded:: 4.0 +UNPROCESSED = UnprocessedParamType() + +#: A unicode string parameter type which is the implicit default. This +#: can also be selected by using ``str`` as type. +STRING = StringParamType() + +#: An integer parameter. This can also be selected by using ``int`` as +#: type. +INT = IntParamType() + +#: A floating point value parameter. This can also be selected by using +#: ``float`` as type. +FLOAT = FloatParamType() + +#: A boolean parameter. This is the default for boolean flags. This can +#: also be selected by using ``bool`` as a type. +BOOL = BoolParamType() + +#: A UUID parameter. +UUID = UUIDParameterType() diff --git a/flask-app/venv/lib/python3.8/site-packages/click/utils.py b/flask-app/venv/lib/python3.8/site-packages/click/utils.py new file mode 100644 index 000000000..16033d623 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/click/utils.py @@ -0,0 +1,579 @@ +import os +import sys +import typing as t +from functools import update_wrapper +from types import ModuleType + +from ._compat import _default_text_stderr +from ._compat import _default_text_stdout +from ._compat import _find_binary_writer +from ._compat import auto_wrap_for_ansi +from ._compat import binary_streams +from ._compat import get_filesystem_encoding +from ._compat import open_stream +from ._compat import should_strip_ansi +from ._compat import strip_ansi +from ._compat import text_streams +from ._compat import WIN +from .globals import resolve_color_default + +if t.TYPE_CHECKING: + import typing_extensions as te + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + + +def _posixify(name: str) -> str: + return "-".join(name.split()).lower() + + +def safecall(func: F) -> F: + """Wraps a function so that it swallows exceptions.""" + + def wrapper(*args, **kwargs): # type: ignore + try: + return func(*args, **kwargs) + except Exception: + pass + + return update_wrapper(t.cast(F, wrapper), func) + + +def make_str(value: t.Any) -> str: + """Converts a value into a valid string.""" + if isinstance(value, bytes): + try: + return value.decode(get_filesystem_encoding()) + except UnicodeError: + return value.decode("utf-8", "replace") + return str(value) + + +def make_default_short_help(help: str, max_length: int = 45) -> str: + """Returns a condensed version of help string.""" + # Consider only the first paragraph. + paragraph_end = help.find("\n\n") + + if paragraph_end != -1: + help = help[:paragraph_end] + + # Collapse newlines, tabs, and spaces. + words = help.split() + + if not words: + return "" + + # The first paragraph started with a "no rewrap" marker, ignore it. + if words[0] == "\b": + words = words[1:] + + total_length = 0 + last_index = len(words) - 1 + + for i, word in enumerate(words): + total_length += len(word) + (i > 0) + + if total_length > max_length: # too long, truncate + break + + if word[-1] == ".": # sentence end, truncate without "..." + return " ".join(words[: i + 1]) + + if total_length == max_length and i != last_index: + break # not at sentence end, truncate with "..." + else: + return " ".join(words) # no truncation needed + + # Account for the length of the suffix. + total_length += len("...") + + # remove words until the length is short enough + while i > 0: + total_length -= len(words[i]) + (i > 0) + + if total_length <= max_length: + break + + i -= 1 + + return " ".join(words[:i]) + "..." + + +class LazyFile: + """A lazy file works like a regular file but it does not fully open + the file but it does perform some basic checks early to see if the + filename parameter does make sense. This is useful for safely opening + files for writing. + """ + + def __init__( + self, + filename: str, + mode: str = "r", + encoding: t.Optional[str] = None, + errors: t.Optional[str] = "strict", + atomic: bool = False, + ): + self.name = filename + self.mode = mode + self.encoding = encoding + self.errors = errors + self.atomic = atomic + self._f: t.Optional[t.IO] + + if filename == "-": + self._f, self.should_close = open_stream(filename, mode, encoding, errors) + else: + if "r" in mode: + # Open and close the file in case we're opening it for + # reading so that we can catch at least some errors in + # some cases early. + open(filename, mode).close() + self._f = None + self.should_close = True + + def __getattr__(self, name: str) -> t.Any: + return getattr(self.open(), name) + + def __repr__(self) -> str: + if self._f is not None: + return repr(self._f) + return f"" + + def open(self) -> t.IO: + """Opens the file if it's not yet open. This call might fail with + a :exc:`FileError`. Not handling this error will produce an error + that Click shows. + """ + if self._f is not None: + return self._f + try: + rv, self.should_close = open_stream( + self.name, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + except OSError as e: # noqa: E402 + from .exceptions import FileError + + raise FileError(self.name, hint=e.strerror) from e + self._f = rv + return rv + + def close(self) -> None: + """Closes the underlying file, no matter what.""" + if self._f is not None: + self._f.close() + + def close_intelligently(self) -> None: + """This function only closes the file if it was opened by the lazy + file wrapper. For instance this will never close stdin. + """ + if self.should_close: + self.close() + + def __enter__(self) -> "LazyFile": + return self + + def __exit__(self, exc_type, exc_value, tb): # type: ignore + self.close_intelligently() + + def __iter__(self) -> t.Iterator[t.AnyStr]: + self.open() + return iter(self._f) # type: ignore + + +class KeepOpenFile: + def __init__(self, file: t.IO) -> None: + self._file = file + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._file, name) + + def __enter__(self) -> "KeepOpenFile": + return self + + def __exit__(self, exc_type, exc_value, tb): # type: ignore + pass + + def __repr__(self) -> str: + return repr(self._file) + + def __iter__(self) -> t.Iterator[t.AnyStr]: + return iter(self._file) + + +def echo( + message: t.Optional[t.Any] = None, + file: t.Optional[t.IO] = None, + nl: bool = True, + err: bool = False, + color: t.Optional[bool] = None, +) -> None: + """Print a message and newline to stdout or a file. This should be + used instead of :func:`print` because it provides better support + for different data, files, and environments. + + Compared to :func:`print`, this does the following: + + - Ensures that the output encoding is not misconfigured on Linux. + - Supports Unicode in the Windows console. + - Supports writing to binary outputs, and supports writing bytes + to text outputs. + - Supports colors and styles on Windows. + - Removes ANSI color and style codes if the output does not look + like an interactive terminal. + - Always flushes the output. + + :param message: The string or bytes to output. Other objects are + converted to strings. + :param file: The file to write to. Defaults to ``stdout``. + :param err: Write to ``stderr`` instead of ``stdout``. + :param nl: Print a newline after the message. Enabled by default. + :param color: Force showing or hiding colors and other styles. By + default Click will remove color if the output does not look like + an interactive terminal. + + .. versionchanged:: 6.0 + Support Unicode output on the Windows console. Click does not + modify ``sys.stdout``, so ``sys.stdout.write()`` and ``print()`` + will still not support Unicode. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + + .. versionadded:: 3.0 + Added the ``err`` parameter. + + .. versionchanged:: 2.0 + Support colors on Windows if colorama is installed. + """ + if file is None: + if err: + file = _default_text_stderr() + else: + file = _default_text_stdout() + + # Convert non bytes/text into the native string type. + if message is not None and not isinstance(message, (str, bytes, bytearray)): + out: t.Optional[t.Union[str, bytes]] = str(message) + else: + out = message + + if nl: + out = out or "" + if isinstance(out, str): + out += "\n" + else: + out += b"\n" + + if not out: + file.flush() + return + + # If there is a message and the value looks like bytes, we manually + # need to find the binary stream and write the message in there. + # This is done separately so that most stream types will work as you + # would expect. Eg: you can write to StringIO for other cases. + if isinstance(out, (bytes, bytearray)): + binary_file = _find_binary_writer(file) + + if binary_file is not None: + file.flush() + binary_file.write(out) + binary_file.flush() + return + + # ANSI style code support. For no message or bytes, nothing happens. + # When outputting to a file instead of a terminal, strip codes. + else: + color = resolve_color_default(color) + + if should_strip_ansi(file, color): + out = strip_ansi(out) + elif WIN: + if auto_wrap_for_ansi is not None: + file = auto_wrap_for_ansi(file) # type: ignore + elif not color: + out = strip_ansi(out) + + file.write(out) # type: ignore + file.flush() + + +def get_binary_stream(name: "te.Literal['stdin', 'stdout', 'stderr']") -> t.BinaryIO: + """Returns a system stream for byte processing. + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + """ + opener = binary_streams.get(name) + if opener is None: + raise TypeError(f"Unknown standard stream '{name}'") + return opener() + + +def get_text_stream( + name: "te.Literal['stdin', 'stdout', 'stderr']", + encoding: t.Optional[str] = None, + errors: t.Optional[str] = "strict", +) -> t.TextIO: + """Returns a system stream for text processing. This usually returns + a wrapped stream around a binary stream returned from + :func:`get_binary_stream` but it also can take shortcuts for already + correctly configured streams. + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + :param encoding: overrides the detected default encoding. + :param errors: overrides the default error mode. + """ + opener = text_streams.get(name) + if opener is None: + raise TypeError(f"Unknown standard stream '{name}'") + return opener(encoding, errors) + + +def open_file( + filename: str, + mode: str = "r", + encoding: t.Optional[str] = None, + errors: t.Optional[str] = "strict", + lazy: bool = False, + atomic: bool = False, +) -> t.IO: + """This is similar to how the :class:`File` works but for manual + usage. Files are opened non lazy by default. This can open regular + files as well as stdin/stdout if ``'-'`` is passed. + + If stdin/stdout is returned the stream is wrapped so that the context + manager will not close the stream accidentally. This makes it possible + to always use the function like this without having to worry to + accidentally close a standard stream:: + + with open_file(filename) as f: + ... + + .. versionadded:: 3.0 + + :param filename: the name of the file to open (or ``'-'`` for stdin/stdout). + :param mode: the mode in which to open the file. + :param encoding: the encoding to use. + :param errors: the error handling for this file. + :param lazy: can be flipped to true to open the file lazily. + :param atomic: in atomic mode writes go into a temporary file and it's + moved on close. + """ + if lazy: + return t.cast(t.IO, LazyFile(filename, mode, encoding, errors, atomic=atomic)) + f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic) + if not should_close: + f = t.cast(t.IO, KeepOpenFile(f)) + return f + + +def get_os_args() -> t.Sequence[str]: + """Returns the argument part of ``sys.argv``, removing the first + value which is the name of the script. + + .. deprecated:: 8.0 + Will be removed in Click 8.1. Access ``sys.argv[1:]`` directly + instead. + """ + import warnings + + warnings.warn( + "'get_os_args' is deprecated and will be removed in Click 8.1." + " Access 'sys.argv[1:]' directly instead.", + DeprecationWarning, + stacklevel=2, + ) + return sys.argv[1:] + + +def format_filename( + filename: t.Union[str, bytes, os.PathLike], shorten: bool = False +) -> str: + """Formats a filename for user display. The main purpose of this + function is to ensure that the filename can be displayed at all. This + will decode the filename to unicode if necessary in a way that it will + not fail. Optionally, it can shorten the filename to not include the + full path to the filename. + + :param filename: formats a filename for UI display. This will also convert + the filename into unicode without failing. + :param shorten: this optionally shortens the filename to strip of the + path that leads up to it. + """ + if shorten: + filename = os.path.basename(filename) + + return os.fsdecode(filename) + + +def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) -> str: + r"""Returns the config folder for the application. The default behavior + is to return whatever is most appropriate for the operating system. + + To give you an idea, for an app called ``"Foo Bar"``, something like + the following folders could be returned: + + Mac OS X: + ``~/Library/Application Support/Foo Bar`` + Mac OS X (POSIX): + ``~/.foo-bar`` + Unix: + ``~/.config/foo-bar`` + Unix (POSIX): + ``~/.foo-bar`` + Windows (roaming): + ``C:\Users\\AppData\Roaming\Foo Bar`` + Windows (not roaming): + ``C:\Users\\AppData\Local\Foo Bar`` + + .. versionadded:: 2.0 + + :param app_name: the application name. This should be properly capitalized + and can contain whitespace. + :param roaming: controls if the folder should be roaming or not on Windows. + Has no affect otherwise. + :param force_posix: if this is set to `True` then on any POSIX system the + folder will be stored in the home folder with a leading + dot instead of the XDG config home or darwin's + application support folder. + """ + if WIN: + key = "APPDATA" if roaming else "LOCALAPPDATA" + folder = os.environ.get(key) + if folder is None: + folder = os.path.expanduser("~") + return os.path.join(folder, app_name) + if force_posix: + return os.path.join(os.path.expanduser(f"~/.{_posixify(app_name)}")) + if sys.platform == "darwin": + return os.path.join( + os.path.expanduser("~/Library/Application Support"), app_name + ) + return os.path.join( + os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), + _posixify(app_name), + ) + + +class PacifyFlushWrapper: + """This wrapper is used to catch and suppress BrokenPipeErrors resulting + from ``.flush()`` being called on broken pipe during the shutdown/final-GC + of the Python interpreter. Notably ``.flush()`` is always called on + ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any + other cleanup code, and the case where the underlying file is not a broken + pipe, all calls and attributes are proxied. + """ + + def __init__(self, wrapped: t.IO) -> None: + self.wrapped = wrapped + + def flush(self) -> None: + try: + self.wrapped.flush() + except OSError as e: + import errno + + if e.errno != errno.EPIPE: + raise + + def __getattr__(self, attr: str) -> t.Any: + return getattr(self.wrapped, attr) + + +def _detect_program_name( + path: t.Optional[str] = None, _main: ModuleType = sys.modules["__main__"] +) -> str: + """Determine the command used to run the program, for use in help + text. If a file or entry point was executed, the file name is + returned. If ``python -m`` was used to execute a module or package, + ``python -m name`` is returned. + + This doesn't try to be too precise, the goal is to give a concise + name for help text. Files are only shown as their name without the + path. ``python`` is only shown for modules, and the full path to + ``sys.executable`` is not shown. + + :param path: The Python file being executed. Python puts this in + ``sys.argv[0]``, which is used by default. + :param _main: The ``__main__`` module. This should only be passed + during internal testing. + + .. versionadded:: 8.0 + Based on command args detection in the Werkzeug reloader. + + :meta private: + """ + if not path: + path = sys.argv[0] + + # The value of __package__ indicates how Python was called. It may + # not exist if a setuptools script is installed as an egg. It may be + # set incorrectly for entry points created with pip on Windows. + if getattr(_main, "__package__", None) is None or ( + os.name == "nt" + and _main.__package__ == "" + and not os.path.exists(path) + and os.path.exists(f"{path}.exe") + ): + # Executed a file, like "python app.py". + return os.path.basename(path) + + # Executed a module, like "python -m example". + # Rewritten by Python from "-m script" to "/path/to/script.py". + # Need to look at main module to determine how it was executed. + py_module = t.cast(str, _main.__package__) + name = os.path.splitext(os.path.basename(path))[0] + + # A submodule like "example.cli". + if name != "__main__": + py_module = f"{py_module}.{name}" + + return f"python -m {py_module.lstrip('.')}" + + +def _expand_args( + args: t.Iterable[str], + *, + user: bool = True, + env: bool = True, + glob_recursive: bool = True, +) -> t.List[str]: + """Simulate Unix shell expansion with Python functions. + + See :func:`glob.glob`, :func:`os.path.expanduser`, and + :func:`os.path.expandvars`. + + This intended for use on Windows, where the shell does not do any + expansion. It may not exactly match what a Unix shell would do. + + :param args: List of command line arguments to expand. + :param user: Expand user home directory. + :param env: Expand environment variables. + :param glob_recursive: ``**`` matches directories recursively. + + .. versionadded:: 8.0 + + :meta private: + """ + from glob import glob + + out = [] + + for arg in args: + if user: + arg = os.path.expanduser(arg) + + if env: + arg = os.path.expandvars(arg) + + matches = glob(arg, recursive=glob_recursive) + + if not matches: + out.append(arg) + else: + out.extend(matches) + + return out diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/INSTALLER b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/LICENSE b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/LICENSE new file mode 100644 index 000000000..5f8fd634a --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/LICENSE @@ -0,0 +1,6 @@ +## License + +This work is dual-licensed under Apache 2.0 or BSD3. +You may select, at your option, one of the above-listed licenses. + +`SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause` diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/LICENSE.Apache b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/LICENSE.Apache new file mode 100644 index 000000000..bff56b543 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/LICENSE.Apache @@ -0,0 +1,200 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2016 Datadog, Inc. + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/LICENSE.BSD3 b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/LICENSE.BSD3 new file mode 100644 index 000000000..e8f3a81c1 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/LICENSE.BSD3 @@ -0,0 +1,24 @@ +Copyright (c) 2016, Datadog +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Datadog nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL DATADOG BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/METADATA b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/METADATA new file mode 100644 index 000000000..dc280cc86 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/METADATA @@ -0,0 +1,59 @@ +Metadata-Version: 2.1 +Name: ddtrace +Version: 0.55.4 +Summary: Datadog tracing code +Home-page: https://github.com/DataDog/dd-trace-py +Author: Datadog, Inc. +Author-email: dev@datadoghq.com +License: BSD +Platform: UNKNOWN +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* +Description-Content-Type: text/markdown +License-File: LICENSE +License-File: LICENSE.Apache +License-File: LICENSE.BSD3 +License-File: NOTICE +Requires-Dist: packaging (>=17.1) +Requires-Dist: tenacity (>=5) +Requires-Dist: attrs (>=19.2.0) +Requires-Dist: six (>=1.12.0) +Requires-Dist: enum34 ; python_version < "3.4" +Requires-Dist: typing ; python_version < "3.5" +Requires-Dist: protobuf (<3.18,>=3) ; python_version < "3.6" +Requires-Dist: pep562 ; python_version < "3.7" +Requires-Dist: funcsigs (>=1.0.0) ; python_version == "2.7" +Requires-Dist: protobuf (>=3) ; python_version >= "3.6" +Provides-Extra: opentracing +Requires-Dist: opentracing (>=2.0.0) ; extra == 'opentracing' + + +# dd-trace-py + +`ddtrace` is Datadog's tracing library for Python. It is used to trace requests +as they flow across web servers, databases and microservices so that developers +have great visibility into bottlenecks and troublesome requests. + +## Getting Started + +For a basic product overview, installation and quick start, check out our +[setup documentation][setup docs]. + +For more advanced usage and configuration, check out our [API +documentation][api docs]. + +For descriptions of terminology used in APM, take a look at the [official +documentation][visualization docs]. + +[setup docs]: https://docs.datadoghq.com/tracing/setup/python/ +[api docs]: https://ddtrace.readthedocs.io/ +[visualization docs]: https://docs.datadoghq.com/tracing/visualization/ + + diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/NOTICE b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/NOTICE new file mode 100644 index 000000000..732c748d4 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/NOTICE @@ -0,0 +1,4 @@ +Datadog dd-trace-py +Copyright 2016-Present Datadog, Inc. + +This product includes software developed at Datadog, Inc. (https://www.datadoghq.com/). diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/RECORD b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/RECORD new file mode 100644 index 000000000..0d5ca7c1e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/RECORD @@ -0,0 +1,718 @@ +../../../bin/ddtrace-run,sha256=DCxg4zGUb2R-EqBb12RAYXqfDmfQmyfxpjQjrfzYIoo,256 +__pycache__/ddtrace_gevent_check.cpython-38.pyc,, +benchmarks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +benchmarks/__pycache__/__init__.cpython-38.pyc,, +benchmarks/bm/__init__.py,sha256=kBMXCwudCv1V4w5NthjQKrPVwmgSCAQCx9hipfSfgcQ,102 +benchmarks/bm/__pycache__/__init__.cpython-38.pyc,, +benchmarks/bm/__pycache__/_scenario.cpython-38.pyc,, +benchmarks/bm/_scenario.py,sha256=_qB7EtQLr8LkNHbTSwSujMopa5cHMvQAG0iaBeYEqO8,2151 +ddtrace-0.55.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +ddtrace-0.55.4.dist-info/LICENSE,sha256=OZvn-IQ0kjk6HmSP8Yi2WQqZBzGRJQmvBO5w9KBdD3o,186 +ddtrace-0.55.4.dist-info/LICENSE.Apache,sha256=5V2RruBHZQIcPyceiv51DjjvdvhgsgS4pnXAOHDuZkQ,11342 +ddtrace-0.55.4.dist-info/LICENSE.BSD3,sha256=J9S_Tq-hhvteDV2W8R0rqht5DZHkmvgdx3gnLZg4j6Q,1493 +ddtrace-0.55.4.dist-info/METADATA,sha256=XimVW6ZXuv5CVb5QgmbS2baIpQRt7zhv8LOgtgLUHyQ,2107 +ddtrace-0.55.4.dist-info/NOTICE,sha256=Wmf6iXVNfb58zWLK5pIkcbqMflb7pl38JhxjMwmjtyc,146 +ddtrace-0.55.4.dist-info/RECORD,, +ddtrace-0.55.4.dist-info/WHEEL,sha256=m4Wk6eOy92yJY87gVAkQnn-qnRb0vl_v2zsmx0hgXfo,109 +ddtrace-0.55.4.dist-info/entry_points.txt,sha256=p5eRDeO9gI2ranksxfzrevqMy7QEYPG5ZQBRNdkyoGk,219 +ddtrace-0.55.4.dist-info/top_level.txt,sha256=oo2f8zjoPEsC1AZq06GweHbkP6VzB8YWL56XtI2i_mQ,40 +ddtrace/__init__.py,sha256=UwhwpKWpnLVoWBy2gTbxY-iSrF80hcWEIsiHdSyhyKg,863 +ddtrace/__pycache__/__init__.cpython-38.pyc,, +ddtrace/__pycache__/_hooks.cpython-38.pyc,, +ddtrace/__pycache__/_version.cpython-38.pyc,, +ddtrace/__pycache__/compat.cpython-38.pyc,, +ddtrace/__pycache__/constants.cpython-38.pyc,, +ddtrace/__pycache__/context.cpython-38.pyc,, +ddtrace/__pycache__/encoding.cpython-38.pyc,, +ddtrace/__pycache__/filters.cpython-38.pyc,, +ddtrace/__pycache__/helpers.cpython-38.pyc,, +ddtrace/__pycache__/monkey.cpython-38.pyc,, +ddtrace/__pycache__/pin.cpython-38.pyc,, +ddtrace/__pycache__/provider.cpython-38.pyc,, +ddtrace/__pycache__/sampler.cpython-38.pyc,, +ddtrace/__pycache__/span.cpython-38.pyc,, +ddtrace/__pycache__/tracer.cpython-38.pyc,, +ddtrace/__pycache__/util.cpython-38.pyc,, +ddtrace/__pycache__/version.cpython-38.pyc,, +ddtrace/_hooks.py,sha256=vpsGwWoomIaHCy689rKM7Ozu3ypV8L8o3O1er3spOxI,3714 +ddtrace/_version.py,sha256=TsdkaNK0rNmQ2RKhnED2V1_DIEzYxpTCDe9G5M1q_Fc,144 +ddtrace/bootstrap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ddtrace/bootstrap/__pycache__/__init__.cpython-38.pyc,, +ddtrace/bootstrap/__pycache__/sitecustomize.cpython-38.pyc,, +ddtrace/bootstrap/sitecustomize.py,sha256=0B3XvRhria2JXUUeYVW4Et1_s6mLOBSiOq-tnl8HkKY,5474 +ddtrace/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ddtrace/commands/__pycache__/__init__.cpython-38.pyc,, +ddtrace/commands/__pycache__/ddtrace_run.cpython-38.pyc,, +ddtrace/commands/ddtrace_run.py,sha256=Z87dzzxQmYmp5WpCoxWSDpLRYG1ASXA33qPA_FJ3API,4461 +ddtrace/compat.py,sha256=4EWK_z7A9rVOSDc8HS7_4nkOV8hbNXq1mTIdFDDKB8o,2321 +ddtrace/constants.py,sha256=LqI4mir6JHcgaTEsXyGWK6tleGf9dp8zCqAa55uV56U,1350 +ddtrace/context.py,sha256=TpuKmScXVPtdonSqwiMRQokq_dELxK5-qm-SdVSG-1w,4155 +ddtrace/contrib/__init__.py,sha256=0SVwa5x1WzoPG2UvBhWXzMlX2OnDioEHjqHqgV1Yn2Q,152 +ddtrace/contrib/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/__pycache__/trace_utils.cpython-38.pyc,, +ddtrace/contrib/__pycache__/util.cpython-38.pyc,, +ddtrace/contrib/aiobotocore/__init__.py,sha256=nHsrBNpYSjcDjCSnqNKzlDzQgRew86V4BoDKqHr9yFg,873 +ddtrace/contrib/aiobotocore/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/aiobotocore/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/aiobotocore/patch.py,sha256=c5dTY7v_Yg-t5YwOLd63Ukd9ygVq72ryZSC7MgVOKFk,4834 +ddtrace/contrib/aiohttp/__init__.py,sha256=ZcRK_PQ4lRGvXBB2A_lCcWcwvk5mdb5rhgRHCgPW5aM,2251 +ddtrace/contrib/aiohttp/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/aiohttp/__pycache__/middlewares.cpython-38.pyc,, +ddtrace/contrib/aiohttp/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/aiohttp/__pycache__/template.cpython-38.pyc,, +ddtrace/contrib/aiohttp/middlewares.py,sha256=is2OvWJTLWPHqK-Cn_6Hroa0KYmLYnkz5iG9LmXzLx0,5262 +ddtrace/contrib/aiohttp/patch.py,sha256=IkvGlK18LrWisBESXVaV8aQUMyg-bwoTEQfp24-6FaI,1364 +ddtrace/contrib/aiohttp/template.py,sha256=iLd-CrDxItNz5bo1Qchu0E_tB8PFEhOSS24sxdYluOE,1024 +ddtrace/contrib/aiopg/__init__.py,sha256=6SzT90nwF01vdG6KSfWFr2iztrPWaLkxXl5EJpafFPU,764 +ddtrace/contrib/aiopg/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/aiopg/__pycache__/connection.cpython-38.pyc,, +ddtrace/contrib/aiopg/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/aiopg/connection.py,sha256=uaukQH7Q2T4WxVhfOcf4w6DGvJv36tiWJMldSxPabWc,3871 +ddtrace/contrib/aiopg/patch.py,sha256=LTFgz1PJhlFPSL_aJ9UVSsAfX3ZG1Jb-Jj7ifEvvGJY,1797 +ddtrace/contrib/algoliasearch/__init__.py,sha256=ETMHk79BfsJzESvck9pUNKVgpevjFx-U0vjaB8P53n8,900 +ddtrace/contrib/algoliasearch/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/algoliasearch/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/algoliasearch/patch.py,sha256=FyJTbGne96QoPJzLtSFi6CgkDOlGVb9bZnorMykhUrk,5072 +ddtrace/contrib/aredis/__init__.py,sha256=qx4q_fdbRyJC1wDppxSxXF1FYp3VFhmwKX-erwL0xck,1318 +ddtrace/contrib/aredis/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/aredis/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/aredis/patch.py,sha256=0VSlhmT0W8j4IrCa1WzgMefXgaJfFrNfQJGZ_hS1Ag4,4611 +ddtrace/contrib/asgi/__init__.py,sha256=EZ_uuOcQ5TvCzx6Xu7MYfs3npHeij9z9L94ygz-m5As,2081 +ddtrace/contrib/asgi/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/asgi/__pycache__/middleware.cpython-38.pyc,, +ddtrace/contrib/asgi/__pycache__/utils.cpython-38.pyc,, +ddtrace/contrib/asgi/middleware.py,sha256=KUADjD4OZv5HIJaQDQx3BD4MX6urSIQiA9O6R3-MUNk,5750 +ddtrace/contrib/asgi/utils.py,sha256=yjwwOejC2irGYD2B-ddoSfFLPKjXpouGKan4iAhjwdM,3273 +ddtrace/contrib/asyncio/__init__.py,sha256=VResycbg3PZkDFy8xCom3aL3dm_1ajC3-QifH3tGkGE,2459 +ddtrace/contrib/asyncio/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/asyncio/__pycache__/compat.cpython-38.pyc,, +ddtrace/contrib/asyncio/__pycache__/helpers.cpython-38.pyc,, +ddtrace/contrib/asyncio/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/asyncio/__pycache__/provider.cpython-38.pyc,, +ddtrace/contrib/asyncio/__pycache__/wrappers.cpython-38.pyc,, +ddtrace/contrib/asyncio/compat.py,sha256=ajEcZZcZyRJJX_QhqdC-43C-0p8sRQZMufDNyhYk9cQ,279 +ddtrace/contrib/asyncio/helpers.py,sha256=G1L7FBnpFVBkn5m9dEFHdXLG1ECY2LANpJAN2H3_nEU,3102 +ddtrace/contrib/asyncio/patch.py,sha256=CxpqQt5h27lo_DdoqLDJil27jFlj4lrb5zzJq-2d2H4,1336 +ddtrace/contrib/asyncio/provider.py,sha256=CIZMUeR3Glm-WrO7PQH0OUMUSUXjI-YMMv73zueWFpA,2855 +ddtrace/contrib/asyncio/wrappers.py,sha256=svG3-7JQ9kM1pFcKYCji-LRGGqe5ZDUTFx4h3qkDpsM,953 +ddtrace/contrib/boto/__init__.py,sha256=QnyFSOLycdIeslkdzLODYjPtfnSgaX2ZU_IfVtOBE2E,684 +ddtrace/contrib/boto/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/boto/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/boto/patch.py,sha256=_28Z946Hw1XVHVZ0pAVL0H_XLMAK9K1J0eVJvhg0b9w,5738 +ddtrace/contrib/botocore/__init__.py,sha256=s0HTe9a7xGdnXljlyxxCmhbDVE_Od5_AzThLk_vseJ4,1560 +ddtrace/contrib/botocore/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/botocore/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/botocore/patch.py,sha256=nJfnOePGxqxn0srmXTQmzZFqZaKn6-eZFw2Ma4R8m14,6297 +ddtrace/contrib/bottle/__init__.py,sha256=pSvNJNq_L7Pp45SPiZLBR1etlZ4SSFu0RwTdbJBg8I0,1077 +ddtrace/contrib/bottle/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/bottle/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/bottle/__pycache__/trace.cpython-38.pyc,, +ddtrace/contrib/bottle/patch.py,sha256=zw6g6t7wseaTZhmbx2bH_GgSRVIdADa9VQFYuKbt8tQ,798 +ddtrace/contrib/bottle/trace.py,sha256=0xcuHbBL1vqLXKZeZXbUEauZgATCQ7zG7SzK9jW2l5g,3574 +ddtrace/contrib/cassandra/__init__.py,sha256=KyK-ud5RY4uJQLCQmR488TSpw9VjakMaeF5J0Uh-9Dw,1226 +ddtrace/contrib/cassandra/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/cassandra/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/cassandra/__pycache__/session.cpython-38.pyc,, +ddtrace/contrib/cassandra/patch.py,sha256=gTEULNQXWMvEH4VQHcvBthVH3XGWytnHLMCum2XcjeQ,89 +ddtrace/contrib/cassandra/session.py,sha256=9qbmQE4zTDCOz80ahQw6gDL-_gUSigVUaf2288fwrqA,9976 +ddtrace/contrib/celery/__init__.py,sha256=tPXuqw3E3Id4lPG4fq_CZWk4LbTBUT_72aEiEdedIFY,1709 +ddtrace/contrib/celery/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/celery/__pycache__/app.cpython-38.pyc,, +ddtrace/contrib/celery/__pycache__/constants.cpython-38.pyc,, +ddtrace/contrib/celery/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/celery/__pycache__/signals.cpython-38.pyc,, +ddtrace/contrib/celery/__pycache__/task.cpython-38.pyc,, +ddtrace/contrib/celery/__pycache__/utils.cpython-38.pyc,, +ddtrace/contrib/celery/app.py,sha256=Bmlt6O5jcyvJV6J607_cmGct_M1-QEYqA0lju4GlQmA,1884 +ddtrace/contrib/celery/constants.py,sha256=hkqiuzQQir6nuyiGL1WMqbFYT_5qYwdbLeqkFxCTa7E,485 +ddtrace/contrib/celery/patch.py,sha256=Skmxw_pFyKwzwwv_2uUrYw2qL-IPsT_9VftA2K4JR3c,1091 +ddtrace/contrib/celery/signals.py,sha256=1LLp__uA9uvYoCQKxDpq-dXy_muujJIzCYZ1geTakjA,7082 +ddtrace/contrib/celery/task.py,sha256=Nr0EuNA_LeO-H9ad0YFiRusk0QcGIbd4v6-AQ6PgBOM,926 +ddtrace/contrib/celery/utils.py,sha256=0GHU1bi9jC4uH6m9M63SOc5WprnOUaVGNePhkmK7vHU,4291 +ddtrace/contrib/cherrypy/__init__.py,sha256=Uzip8JhUAylwcAI2HOuWj8X-mURKzNXcNdk3Ag7_0p4,1562 +ddtrace/contrib/cherrypy/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/cherrypy/__pycache__/middleware.cpython-38.pyc,, +ddtrace/contrib/cherrypy/middleware.py,sha256=U4RWepJa-uEHfnicibJ_UNJhk8km6e6aeOa57-OIWGo,5061 +ddtrace/contrib/consul/__init__.py,sha256=SY3yeLgOQrlNCY46IWax2U4581dY5ewQu_4QBBfmong,834 +ddtrace/contrib/consul/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/consul/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/consul/patch.py,sha256=v6nZ7fIZJLqQJEJTOdqr0EnUJSEU2e2C1fWS4XhLlpo,1859 +ddtrace/contrib/dbapi/__init__.py,sha256=2QQiIPsHDtln4b330or6Wym5xvwFqQjRi2dGY0Slgzk,10940 +ddtrace/contrib/dbapi/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/django/__init__.py,sha256=ieA39ryf9_YNKvejV_BKmHxV-2v85eP9bVKMPerCkhc,4357 +ddtrace/contrib/django/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/django/__pycache__/_asgi.cpython-38.pyc,, +ddtrace/contrib/django/__pycache__/compat.cpython-38.pyc,, +ddtrace/contrib/django/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/django/__pycache__/restframework.cpython-38.pyc,, +ddtrace/contrib/django/__pycache__/utils.cpython-38.pyc,, +ddtrace/contrib/django/_asgi.py,sha256=ZvFX2eM4A6aZb5ngLwzeVB_WXkPvzb918u1ls1iwLzc,1381 +ddtrace/contrib/django/compat.py,sha256=vooC381G_q8Y8d9gakb1UTdvrXBLo2s7VuAq--UFEjI,909 +ddtrace/contrib/django/patch.py,sha256=VVsUMOTERjnW3XYRGU3ipsrOsMgX-nv5Pofp1P_gLno,21419 +ddtrace/contrib/django/restframework.py,sha256=uWiWt9D58NGLtDtjlZBBiIA-B9KogQjRTVq7yjTECk8,1159 +ddtrace/contrib/django/utils.py,sha256=aXHPvEHwTe2T8XwU0pZHpHyC-dO4iEP0E07x_o3w8PM,11308 +ddtrace/contrib/dogpile_cache/__init__.py,sha256=Wmx5yn7DPO61IsgbZGjFV4jVzoCy_iSDdjoIt9vUmec,1590 +ddtrace/contrib/dogpile_cache/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/dogpile_cache/__pycache__/lock.cpython-38.pyc,, +ddtrace/contrib/dogpile_cache/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/dogpile_cache/__pycache__/region.cpython-38.pyc,, +ddtrace/contrib/dogpile_cache/lock.py,sha256=KsNQ8vBFAAg-8s9SMXjiNFbKALCMWEVhnBd4ObD0clc,1627 +ddtrace/contrib/dogpile_cache/patch.py,sha256=X_l14SqEq6yghMBH5fbK4tZ0hdqDVLImsolLTlrfOEE,1550 +ddtrace/contrib/dogpile_cache/region.py,sha256=YSWL4qdBpz2pwqtkBF9EfOpvL5CWjwz8j8jV2Nrb73U,1279 +ddtrace/contrib/elasticsearch/__init__.py,sha256=Zhyn6H4XIPysGbd7Q8pW9ZJ9MpGb7sg0M0-W_WIPZnM,815 +ddtrace/contrib/elasticsearch/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/elasticsearch/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/elasticsearch/__pycache__/quantize.cpython-38.pyc,, +ddtrace/contrib/elasticsearch/patch.py,sha256=OdB5Pzqq7Uv-6Y83Ce-VfYeierUnDiRw50u4ASu_t6E,3930 +ddtrace/contrib/elasticsearch/quantize.py,sha256=wdiGa9N2CcPXw614lwb6-kCTBrxvkubtmb4KI2S9H5c,1052 +ddtrace/contrib/falcon/__init__.py,sha256=kPO-5D436j8mKChnXV2yqsfD3zAH5rVtSmASksl2qKQ,1457 +ddtrace/contrib/falcon/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/falcon/__pycache__/middleware.cpython-38.pyc,, +ddtrace/contrib/falcon/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/falcon/middleware.py,sha256=bK1PtI9mdUuHJAC656LQWTSnuqcfJhBUgqpQIR0xkIQ,3796 +ddtrace/contrib/falcon/patch.py,sha256=c7qLMRpg5WUMXq2HiRkFwsazIDlAATTQ7n3tUtTAkYs,1145 +ddtrace/contrib/fastapi/__init__.py,sha256=B_AwffCU3WMUb0Y4fyPPlwXdyxbAdSzoFDQDbrOnanc,1604 +ddtrace/contrib/fastapi/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/fastapi/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/fastapi/patch.py,sha256=0mqiMyNVlwmWjSOn_Eb251NH13qUw3dQDO6R37xXdqs,1418 +ddtrace/contrib/flask/__init__.py,sha256=Qqq0SpucjfPR-qrhLNJJ42H7bVtBx6iWXd91krF5lRg,2330 +ddtrace/contrib/flask/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/flask/__pycache__/helpers.cpython-38.pyc,, +ddtrace/contrib/flask/__pycache__/middleware.cpython-38.pyc,, +ddtrace/contrib/flask/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/flask/__pycache__/wrappers.cpython-38.pyc,, +ddtrace/contrib/flask/helpers.py,sha256=e6bWhOPsqSjlRkSsMoxfcNyGTRMqRQUebYkiVtPGr_w,1088 +ddtrace/contrib/flask/middleware.py,sha256=9BBGncZf9T197BGj98uXJzzAG6UU_T3BqyEjZ3YLgFU,8571 +ddtrace/contrib/flask/patch.py,sha256=ciu70oD0VqwBpGAiNFe-xf3s0StvznlJft0Cy5xkXjw,19164 +ddtrace/contrib/flask/wrappers.py,sha256=-UuUp3_rkTjPWseztsKk-IyUb47sQ_EPHJ6ZS1T70mc,1537 +ddtrace/contrib/flask_cache/__init__.py,sha256=z7ajIF6vddKAUl0KDTYSpbFnpSEiiBtJnhiTVhqCL10,1637 +ddtrace/contrib/flask_cache/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/flask_cache/__pycache__/tracers.cpython-38.pyc,, +ddtrace/contrib/flask_cache/__pycache__/utils.cpython-38.pyc,, +ddtrace/contrib/flask_cache/tracers.py,sha256=HXoXJVpNw0FDZrbxWFS2A31gvCRD2yXqHkTKpk9umYc,5796 +ddtrace/contrib/flask_cache/utils.py,sha256=bhCIB8lu7ov6bfvTa-J_u-elnevQM4_iAZaohHV1eqI,1936 +ddtrace/contrib/futures/__init__.py,sha256=6nUV3-p49uKvkUNgKtNKdm2GKTxrkrtVeyQgcgO0gWg,821 +ddtrace/contrib/futures/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/futures/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/futures/__pycache__/threading.cpython-38.pyc,, +ddtrace/contrib/futures/patch.py,sha256=WbGoW88ASbHGQRk9IyDPYxAIkmuCAR0mQNEQpct4fbQ,662 +ddtrace/contrib/futures/threading.py,sha256=pMA0E9akmNnE8JJDCsgWJXVFop-fg5FEidkC-fkn0p8,1722 +ddtrace/contrib/gevent/__init__.py,sha256=94AzepjMNJa5E9nKNNcLllfaos0LrYkIAMfcJkEcttA,2126 +ddtrace/contrib/gevent/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/gevent/__pycache__/greenlet.cpython-38.pyc,, +ddtrace/contrib/gevent/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/gevent/__pycache__/provider.cpython-38.pyc,, +ddtrace/contrib/gevent/greenlet.py,sha256=rEzvw7pibHIXNe8tOK4Jm9J7Cb4pK6_nt-me8p6lNVE,2241 +ddtrace/contrib/gevent/patch.py,sha256=_ximL6YK_1NVIBmzgF_vIBpCQ4yG1M4RqddmBMmMC6c,2244 +ddtrace/contrib/gevent/provider.py,sha256=wWh_EnT4xdpekgdJhiqGWnTj2CXyUfLlYJAVYX2WrjA,1468 +ddtrace/contrib/grpc/__init__.py,sha256=bO2KTcKKrZCoI35dx69kbKlsMgBbz-xlYMnOrGmSIXI,2166 +ddtrace/contrib/grpc/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/grpc/__pycache__/client_interceptor.cpython-38.pyc,, +ddtrace/contrib/grpc/__pycache__/constants.cpython-38.pyc,, +ddtrace/contrib/grpc/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/grpc/__pycache__/server_interceptor.cpython-38.pyc,, +ddtrace/contrib/grpc/__pycache__/utils.cpython-38.pyc,, +ddtrace/contrib/grpc/client_interceptor.py,sha256=uYbYHBqFptvg1iCrHLVOI1NjSZjywkZx6D65dKkp34w,10297 +ddtrace/contrib/grpc/constants.py,sha256=9ii-eBuPm8UzuXJQGzeoxPVQNHbA9pMxeFDIiSUvotg,908 +ddtrace/contrib/grpc/patch.py,sha256=2b8Rtd5ZIlzYe4cjHcPXrptPt_WV1-Gpa9AuBlEpxhs,3391 +ddtrace/contrib/grpc/server_interceptor.py,sha256=vtOXvAs5yQDtHF64eVCORkOz9PO6wN552IiSs5LH0u0,4664 +ddtrace/contrib/grpc/utils.py,sha256=U67Ijho-_W2V11oHEPOY7nrOMqjOzGmXXL_GB9MNRRY,2509 +ddtrace/contrib/httplib/__init__.py,sha256=W_A6MCkb438Z95IT3VntRU7uVMDWE8ZPTpiPxZcfLvI,1506 +ddtrace/contrib/httplib/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/httplib/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/httplib/patch.py,sha256=PSPuqhg2SGHgODxl5sdr6sEb5V1uRemBQnhaxhOySNM,7065 +ddtrace/contrib/httpx/__init__.py,sha256=Ix0IbVW7W-7C77QtzXD1CdZ6XUr0UfS23XOfqEwG9p0,2498 +ddtrace/contrib/httpx/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/httpx/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/httpx/patch.py,sha256=O-1fjyX166k-wsfkyrsRV5qa8i2mr7_xMd_GafBnHAA,4644 +ddtrace/contrib/jinja2/__init__.py,sha256=OUQCQku8w5Cl2CzkwY5wtbcFdDUfOCXRAABpPzi9K-A,1245 +ddtrace/contrib/jinja2/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/jinja2/__pycache__/constants.cpython-38.pyc,, +ddtrace/contrib/jinja2/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/jinja2/constants.py,sha256=BpRqO-bQIWzrHFO7DtJAZ4v7lH7HpYWjYri7yCOG-Ts,35 +ddtrace/contrib/jinja2/patch.py,sha256=csTFVcvuK9H4AS6V33izdHYYhe-giXQg6KlKiUGDZyI,3214 +ddtrace/contrib/kombu/__init__.py,sha256=9dkzHdyCC7-X_V58IUalMD_9tGG5_nCcpwfnbUre-7g,1608 +ddtrace/contrib/kombu/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/kombu/__pycache__/constants.cpython-38.pyc,, +ddtrace/contrib/kombu/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/kombu/__pycache__/utils.cpython-38.pyc,, +ddtrace/contrib/kombu/constants.py,sha256=RhhLOyKsbyFLnko4wvuyDQvBv_wy0CMN27k4akMSlak,26 +ddtrace/contrib/kombu/patch.py,sha256=gYEHxPMPcd8rEpAccpmCOxqAvnnsvjVF86sfqeqAquQ,4321 +ddtrace/contrib/kombu/utils.py,sha256=xjAzg2zr7o74Iy7Bc2vIQvfvm_W82ZzvbiBHLNtCa7E,1101 +ddtrace/contrib/logging/__init__.py,sha256=n99351HcYyMUL6ZFysiJdvFcg499vO9klZKs1PQfk5E,2166 +ddtrace/contrib/logging/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/logging/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/logging/patch.py,sha256=FzFAFgLvJGtOx2Z1OboiYJeuOWXGR6il6TUUK51kGbc,4320 +ddtrace/contrib/mako/__init__.py,sha256=s2C8nifhGMO1O5mCP4BTx7EOMYLUOfmiwkgljDw3IFI,588 +ddtrace/contrib/mako/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/mako/__pycache__/constants.cpython-38.pyc,, +ddtrace/contrib/mako/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/mako/constants.py,sha256=BpRqO-bQIWzrHFO7DtJAZ4v7lH7HpYWjYri7yCOG-Ts,35 +ddtrace/contrib/mako/patch.py,sha256=0pNus4f2KjbKj9vCk-UCK5PrVoayATbRx1ry_yDaj8I,2080 +ddtrace/contrib/mariadb/__init__.py,sha256=PXwfQKozHsQNq6rh2Px_w-xHw8XQnmsr27RmRtnnadU,1634 +ddtrace/contrib/mariadb/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/mariadb/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/mariadb/patch.py,sha256=RkSagzkI_qDd5JjpT_KH596ZHLOCoa3na14npYK_6qU,1250 +ddtrace/contrib/molten/__init__.py,sha256=_MGjAFUuzSFbtX30vNhw4mmU5sLMPMFpjoqj0KvJTHo,1268 +ddtrace/contrib/molten/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/molten/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/molten/__pycache__/wrappers.cpython-38.pyc,, +ddtrace/contrib/molten/patch.py,sha256=FsBkLJHcego8o2z3HuxsNRX1J9mEzSAUt62qAt86ZyM,5225 +ddtrace/contrib/molten/wrappers.py,sha256=VdJfn6waJZK1-d4XT0JWMl-iBqoVi8PJNQ2XmC0y1eU,3312 +ddtrace/contrib/mongoengine/__init__.py,sha256=Bk5uz8reJ2fRE5FTrQG4j2V_G9zQrlLgNvArlw5PBZ8,890 +ddtrace/contrib/mongoengine/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/mongoengine/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/mongoengine/__pycache__/trace.cpython-38.pyc,, +ddtrace/contrib/mongoengine/patch.py,sha256=0EnBafNG834UUGqUlp5PEfU8pjSs2Z2rM--JJl_9GmI,438 +ddtrace/contrib/mongoengine/trace.py,sha256=cMIIt5Dp8yWM0kmIEys-31wPXPQKlf5WwcqmdX8iF7U,1170 +ddtrace/contrib/mysql/__init__.py,sha256=fwelUWtASBH613-O0L3yKkwGtOyt0mkOhdX6E2yox9I,2076 +ddtrace/contrib/mysql/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/mysql/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/mysql/__pycache__/tracers.cpython-38.pyc,, +ddtrace/contrib/mysql/patch.py,sha256=QEO3EX9mdeHEqtg01-Vz0mjth1Cv9AKKeuHqi5Pc36k,1512 +ddtrace/contrib/mysql/tracers.py,sha256=xXVri5QBHcAJGRBzq8aQ6m8r7jauaxNU0jWlqK4H-VU,240 +ddtrace/contrib/mysqldb/__init__.py,sha256=SpGcfRxVB0mqu_q3z5NV-aoVdsL51z9dizPt7h-7B38,1893 +ddtrace/contrib/mysqldb/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/mysqldb/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/mysqldb/patch.py,sha256=WSTdttV6k-A4IajuuxzQDG6RbTXCcmXfP0sWSBrT8bg,1920 +ddtrace/contrib/psycopg/__init__.py,sha256=CKkYbGpZGDAl8Ig7O07CYS5WzYxAnlBN_gOsNzhuc60,1613 +ddtrace/contrib/psycopg/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/psycopg/__pycache__/connection.cpython-38.pyc,, +ddtrace/contrib/psycopg/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/psycopg/connection.py,sha256=hv8nR0ZkFMbOHrmd-F-rMlq90kzqYAm7MWFhX9gevFo,3283 +ddtrace/contrib/psycopg/patch.py,sha256=93fT6GCpOIsozqV6l3tOev1pTMQDgYCTqjE1GmPgLpI,6201 +ddtrace/contrib/pylibmc/__init__.py,sha256=wbT3awnMVI67TAQiJcg1_amnY9cLowvoqYZloD2ADtA,944 +ddtrace/contrib/pylibmc/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/pylibmc/__pycache__/addrs.cpython-38.pyc,, +ddtrace/contrib/pylibmc/__pycache__/client.cpython-38.pyc,, +ddtrace/contrib/pylibmc/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/pylibmc/addrs.py,sha256=S1wHEMy_T_zZWom_p6_u9eyC1W39nn_sXE9gC9nsTGg,365 +ddtrace/contrib/pylibmc/client.py,sha256=7m60ghYKqT4FCz4gSV65R0m696eElsdfclgMO7TWscE,5561 +ddtrace/contrib/pylibmc/patch.py,sha256=7r-aJQULmY8A7JGvcOcWk1bOnMvcRJOdLNbAFRkxcLU,217 +ddtrace/contrib/pylons/__init__.py,sha256=Dm_uea1ZTslO5yiYTfB0OuOQDvosl-jHR44NXvsQYEA,1660 +ddtrace/contrib/pylons/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/pylons/__pycache__/compat.cpython-38.pyc,, +ddtrace/contrib/pylons/__pycache__/constants.cpython-38.pyc,, +ddtrace/contrib/pylons/__pycache__/middleware.cpython-38.pyc,, +ddtrace/contrib/pylons/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/pylons/__pycache__/renderer.cpython-38.pyc,, +ddtrace/contrib/pylons/compat.py,sha256=yDoHyo3R7FNYpqCoOOz4HxTNhfj8TpMJW5Jqs64gUMw,174 +ddtrace/contrib/pylons/constants.py,sha256=BQOdVHL_aXDLZpjE7q4XFCXxo0aAe79HqltMudK5HNY,43 +ddtrace/contrib/pylons/middleware.py,sha256=ImH2DGKet9WLbtVOjceRRniEuJKDIw0i1twsRXXcVkg,4889 +ddtrace/contrib/pylons/patch.py,sha256=twvUNWOiGJZYdAO9XKOYn53fJ_9qvW9hspmvAI7gSHA,1457 +ddtrace/contrib/pylons/renderer.py,sha256=ro_4C8noWFRHuAheM5dQz-MPfDp8rW2hUy12rQtnIvQ,1268 +ddtrace/contrib/pymemcache/__init__.py,sha256=kfbpc_SFdnw7DE929sYm0bI2sUxJefz4vcRt_mIseQo,1150 +ddtrace/contrib/pymemcache/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/pymemcache/__pycache__/client.cpython-38.pyc,, +ddtrace/contrib/pymemcache/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/pymemcache/client.py,sha256=Bd3MxhoyC3Winl73VK1EhA5W1Kw8ZjC80Xkm7kprKbM,6801 +ddtrace/contrib/pymemcache/patch.py,sha256=A3hEoAG11TxtnERDGbZbqgwg-vPu7lB-j_J6_k_hgdw,1609 +ddtrace/contrib/pymongo/__init__.py,sha256=YpUSCktAoFL_M7EuyNhRTJE70AX2XL4FETecntAcbfM,1231 +ddtrace/contrib/pymongo/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/pymongo/__pycache__/client.cpython-38.pyc,, +ddtrace/contrib/pymongo/__pycache__/parse.cpython-38.pyc,, +ddtrace/contrib/pymongo/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/pymongo/client.py,sha256=kFOl8-WKgQ9GJSTCuUr2YTZqzGpRYhyiucmu3aZTxo8,9548 +ddtrace/contrib/pymongo/parse.py,sha256=kJccHF7TKlWnfrN75duPAkRQeKph75h3LyzON0y_cBs,6292 +ddtrace/contrib/pymongo/patch.py,sha256=seZrRnJbukNK_WPsbW8tQ3rQgg_UjaiPPM_th0YBjso,2405 +ddtrace/contrib/pymysql/__init__.py,sha256=IloQB5S4PKR7hY4iVoItfoTzKqvfA-wj3x103oQZbqQ,1684 +ddtrace/contrib/pymysql/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/pymysql/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/pymysql/__pycache__/tracers.cpython-38.pyc,, +ddtrace/contrib/pymysql/patch.py,sha256=Sl8gy_3NXFAfuZXZ0d7SA7sDbva3jOUjBVCcQRMTPxs,1226 +ddtrace/contrib/pymysql/tracers.py,sha256=tAy6B-VAJywrhPdBR4esB9gHKbhhXSQ9XgRSgTtW6DA,245 +ddtrace/contrib/pynamodb/__init__.py,sha256=3xCQXBLKhLH3ER1CmrryRf227U_TzBCG8WYHJ0YSAcw,937 +ddtrace/contrib/pynamodb/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/pynamodb/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/pynamodb/patch.py,sha256=7eEir_rwjZbDhBKEFvP9udy_wqW_BlXwooGCNzHbZsk,2587 +ddtrace/contrib/pyodbc/__init__.py,sha256=VfyetYAFAcKfpsNOtKZxtXaBarQzPb-M5D-B-U3lHbk,1522 +ddtrace/contrib/pyodbc/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/pyodbc/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/pyodbc/patch.py,sha256=FMBGx1xRVDW_EqPh1VAtdotzLs_FIwp2Gk7zw7uU57M,1350 +ddtrace/contrib/pyramid/__init__.py,sha256=jLvdWS3hef7m2Uyi_QbLpXATxdJXJLioSUZ5EaHtCxo,1706 +ddtrace/contrib/pyramid/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/pyramid/__pycache__/constants.cpython-38.pyc,, +ddtrace/contrib/pyramid/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/pyramid/__pycache__/trace.cpython-38.pyc,, +ddtrace/contrib/pyramid/constants.py,sha256=69TDS3jZ0kf6M7WlNT60Nv4qOWIiGEcbsCeG8_Qp9vw,310 +ddtrace/contrib/pyramid/patch.py,sha256=EvirLmtpwptjZZyzOVNj8oJbwQGNMDCqcONDs1IXT2Q,3523 +ddtrace/contrib/pyramid/trace.py,sha256=gMfnmhQ7HEqXos1ln17ewR_rM69brlVpx-ADPK7IXLI,4950 +ddtrace/contrib/pytest/__init__.py,sha256=IYqgSShVB6_Peu_hcMrQZxRrlJa45mKU3XplNxQa5e8,1381 +ddtrace/contrib/pytest/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/pytest/__pycache__/constants.cpython-38.pyc,, +ddtrace/contrib/pytest/__pycache__/plugin.cpython-38.pyc,, +ddtrace/contrib/pytest/constants.py,sha256=g0fRwf78PvNSr8HKd2Cuv7sDjGZo_IcM6jsrfvk1P5E,85 +ddtrace/contrib/pytest/plugin.py,sha256=00vrONmqM-CWlLUKtQv2L6emPmatSxSFBsWeaDTIjZo,7298 +ddtrace/contrib/redis/__init__.py,sha256=nBzvVA8tNXXjLWWYIn--bz3jVW8c-W-f4jan0uElpwk,1413 +ddtrace/contrib/redis/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/redis/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/redis/__pycache__/tracers.cpython-38.pyc,, +ddtrace/contrib/redis/__pycache__/util.cpython-38.pyc,, +ddtrace/contrib/redis/patch.py,sha256=Jtye0b_560Q79fb3_Fclta6xNyk_qCF0Re6lnQzqzDQ,4248 +ddtrace/contrib/redis/tracers.py,sha256=7Cg45xIuanDQA_loi_uPPjA28KGNbeNCkDSRuS6Y6As,627 +ddtrace/contrib/redis/util.py,sha256=rWVbittga1H-1MADSNm_YE61C7w-UCk9m0OTR9VTDkc,1350 +ddtrace/contrib/rediscluster/__init__.py,sha256=I0BnwxwhzWOWjBBFFVgrOfqso0mz43VUAgHjxXm4eAA,846 +ddtrace/contrib/rediscluster/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/rediscluster/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/rediscluster/patch.py,sha256=iG-e7BP7b44qmDEStXuxeWVE1KWk0HDMonsNuUPCAIo,3068 +ddtrace/contrib/requests/__init__.py,sha256=ZJrpCXrjOUUS5eqGxL1fdZuCdb-bnavbgab50ybTT-w,2200 +ddtrace/contrib/requests/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/requests/__pycache__/connection.cpython-38.pyc,, +ddtrace/contrib/requests/__pycache__/constants.cpython-38.pyc,, +ddtrace/contrib/requests/__pycache__/legacy.cpython-38.pyc,, +ddtrace/contrib/requests/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/requests/__pycache__/session.cpython-38.pyc,, +ddtrace/contrib/requests/connection.py,sha256=23LmLon9Kw-_bm3FBVGNEYazmLY1tov0wnCO2MGuDk4,3824 +ddtrace/contrib/requests/constants.py,sha256=HY71FxxxJKkCVoCNspOczEf9Ji6QcVDTeDCgYyuA0qU,29 +ddtrace/contrib/requests/legacy.py,sha256=PBJnJWYqYnknrb4z3nHzFyrNjT98520dtP3dNE8vt2k,1096 +ddtrace/contrib/requests/patch.py,sha256=tFTEkMMKHBgujSXUUAJMgwmL3VT46WGdE4h4mD4oEw4,1499 +ddtrace/contrib/requests/session.py,sha256=_vtXsnkadx3J6cJXjDcz39VKPQIzMVLWf0XyB-hOXm8,512 +ddtrace/contrib/rq/__init__.py,sha256=DiPFFDtb6uMNMRxWUh-r_R86g5TDdrdm5iueHE8qkcY,7169 +ddtrace/contrib/rq/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/sanic/__init__.py,sha256=cpw9fqVoCWWvOOqm83EFP-Mh4yR-SmiPYGK38CsgVRw,1769 +ddtrace/contrib/sanic/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/sanic/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/sanic/patch.py,sha256=o-aDW78nxgt0PFExL4E1YcpZsSIHWKzX-7aGwQwsbaE,5825 +ddtrace/contrib/snowflake/__init__.py,sha256=LAVyxarL6ZrWZ0vEduhT-SAd061j39_Ye-7dGJ2ThnM,1834 +ddtrace/contrib/snowflake/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/snowflake/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/snowflake/patch.py,sha256=yOixIjVZCrppxPe_W7am9VYH6szMVC36pLxuUuyx5N4,1686 +ddtrace/contrib/sqlalchemy/__init__.py,sha256=Eiet6PvnV-cmFWACE2mud6PWTl-gA8FJsjeVMd8hiQo,906 +ddtrace/contrib/sqlalchemy/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/sqlalchemy/__pycache__/engine.cpython-38.pyc,, +ddtrace/contrib/sqlalchemy/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/sqlalchemy/engine.py,sha256=Ok_4YYJtronT2AZtIqe6EGPWiAGFR_JMl6vmibpiZas,4945 +ddtrace/contrib/sqlalchemy/patch.py,sha256=Kc2Iawu6Lt2kzjZSj5jC9wAYCke24KUMgI66X4xvtkU,731 +ddtrace/contrib/sqlite3/__init__.py,sha256=MmNK2B_LRgtG3GFvBDCNi4pnHQKuiwkIUaQhr_MRdfo,1404 +ddtrace/contrib/sqlite3/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/sqlite3/__pycache__/connection.cpython-38.pyc,, +ddtrace/contrib/sqlite3/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/sqlite3/connection.py,sha256=7FLn4TPtC5Wfrt-34uxjiq9QJd9pQEumcy6C-7XuqPw,218 +ddtrace/contrib/sqlite3/patch.py,sha256=eOffvuER_PiTLIlEjwngfzFxDqK39pybNowM4m5AV28,2122 +ddtrace/contrib/starlette/__init__.py,sha256=l4JthS_Z0AczkwQD0g3gB2crgCGB48n48bmhLf0ZPT8,2239 +ddtrace/contrib/starlette/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/starlette/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/starlette/patch.py,sha256=pETjY-b0lCC70iRr1cKpjVNo3KbQoR2ONAMvVdTjsjg,1750 +ddtrace/contrib/tornado/__init__.py,sha256=5_aD300_AdzGCIa9jvuRq0_OFSRTuctb7GaqauBMz2E,4530 +ddtrace/contrib/tornado/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/tornado/__pycache__/application.cpython-38.pyc,, +ddtrace/contrib/tornado/__pycache__/compat.cpython-38.pyc,, +ddtrace/contrib/tornado/__pycache__/constants.cpython-38.pyc,, +ddtrace/contrib/tornado/__pycache__/decorators.cpython-38.pyc,, +ddtrace/contrib/tornado/__pycache__/handlers.cpython-38.pyc,, +ddtrace/contrib/tornado/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/tornado/__pycache__/stack_context.cpython-38.pyc,, +ddtrace/contrib/tornado/__pycache__/template.cpython-38.pyc,, +ddtrace/contrib/tornado/application.py,sha256=Ck3kofSMHPzr68Brrl9i0OKUAAgsJlqdm2VFEfy-MWc,1773 +ddtrace/contrib/tornado/compat.py,sha256=aujAgqKEOXeL3XuZKHFcijlEWVW-Wv20d2pPeCX1ayY,360 +ddtrace/contrib/tornado/constants.py,sha256=PsZUkpzC0uUfshu3QrhvPMj5b669K9w1pXGHgitaK00,205 +ddtrace/contrib/tornado/decorators.py,sha256=N859tWDEDwBAH-mSdAD7SJ-P0xx6MfY1e2oKNZLXAyc,3225 +ddtrace/contrib/tornado/handlers.py,sha256=Fjc20khMtgSk4XdfDCOwQHtF5m_askOWg9t6L6Bo6DA,4272 +ddtrace/contrib/tornado/patch.py,sha256=JzhMRht6qYnp8O_X0onx0Mh8SkzkWVFmmgKTjf3Ca8Y,2111 +ddtrace/contrib/tornado/stack_context.py,sha256=BwYFLRN_H2FoTWVqRumLEC1vWbOF0bEPTyqoWGh79Zc,6113 +ddtrace/contrib/tornado/template.py,sha256=4oE6gnM7YrLtbsMsdP8xmRHGLIfLkB08uTJiynJ1ouA,1017 +ddtrace/contrib/trace_utils.py,sha256=P6aAKM8TPa9uIyFY9P_hqpzdxZN3StLSUBWaE-5qw_s,11813 +ddtrace/contrib/urllib3/__init__.py,sha256=OdDmQM7aNwYk23xWkgnhOox6lRAGL92FQiTmnIxbJZQ,1569 +ddtrace/contrib/urllib3/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/urllib3/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/urllib3/patch.py,sha256=ThzuJQ63gqmxez_zwoT8zV9fotvIv81DvJMgdZJ-SaM,4467 +ddtrace/contrib/util.py,sha256=8sn1gyl9jDAprk3sG4oF5WkqMmDxpaw0ogD2KXBUDPk,438 +ddtrace/contrib/vertica/__init__.py,sha256=ltvuezAMIy7Re1LPQxf6OG7H5nXTQyTUWsoJIQcIP1Y,1347 +ddtrace/contrib/vertica/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/vertica/__pycache__/constants.cpython-38.pyc,, +ddtrace/contrib/vertica/__pycache__/patch.cpython-38.pyc,, +ddtrace/contrib/vertica/constants.py,sha256=DNXQ4AR1Luf7KKq9If70nxsKcDxIZF8b__AlIJzgsxw,31 +ddtrace/contrib/vertica/patch.py,sha256=fH_07TMRwau3Smu71QWZB5F8UHiiad8HCzDzSy4vyRw,7826 +ddtrace/contrib/wsgi/__init__.py,sha256=OV9x6jGLh3zV_7LEbkgFraoevCZIUKF5yOH55cw2EB4,830 +ddtrace/contrib/wsgi/__pycache__/__init__.cpython-38.pyc,, +ddtrace/contrib/wsgi/__pycache__/wsgi.cpython-38.pyc,, +ddtrace/contrib/wsgi/wsgi.py,sha256=Ajhld7OzQY1kFiJSsvX8GMnE9HBN7IaubD5IwUtTSNc,5130 +ddtrace/encoding.py,sha256=nQWQw04nBrvJHR6GkvREJrplKocPJk95dP-tzheDJlE,513 +ddtrace/ext/__init__.py,sha256=CW4WZyARim4DC7NBBfwZ18A_bWXf9FYEp-xsLl4z_m8,309 +ddtrace/ext/__pycache__/__init__.cpython-38.pyc,, +ddtrace/ext/__pycache__/aws.cpython-38.pyc,, +ddtrace/ext/__pycache__/cassandra.cpython-38.pyc,, +ddtrace/ext/__pycache__/ci.cpython-38.pyc,, +ddtrace/ext/__pycache__/consul.cpython-38.pyc,, +ddtrace/ext/__pycache__/db.cpython-38.pyc,, +ddtrace/ext/__pycache__/elasticsearch.cpython-38.pyc,, +ddtrace/ext/__pycache__/errors.cpython-38.pyc,, +ddtrace/ext/__pycache__/git.cpython-38.pyc,, +ddtrace/ext/__pycache__/http.cpython-38.pyc,, +ddtrace/ext/__pycache__/kombu.cpython-38.pyc,, +ddtrace/ext/__pycache__/memcached.cpython-38.pyc,, +ddtrace/ext/__pycache__/mongo.cpython-38.pyc,, +ddtrace/ext/__pycache__/net.cpython-38.pyc,, +ddtrace/ext/__pycache__/priority.cpython-38.pyc,, +ddtrace/ext/__pycache__/redis.cpython-38.pyc,, +ddtrace/ext/__pycache__/sql.cpython-38.pyc,, +ddtrace/ext/__pycache__/system.cpython-38.pyc,, +ddtrace/ext/__pycache__/test.cpython-38.pyc,, +ddtrace/ext/aws.py,sha256=Qr0aaE7Y5kky7cpKT25BWvqXoCwhD7xIhUXmOPJZA4M,1400 +ddtrace/ext/cassandra.py,sha256=pImop6ncMOKJa-nAZBCL_9_i9HfMHF-x0rA5W8Cqvpc,225 +ddtrace/ext/ci.py,sha256=6huk4wee-zEA_p9tphpKmWdPx0LGz_p2SLdyOxeggyc,16796 +ddtrace/ext/consul.py,sha256=6Vq9cGCCOimwFMyQRR1rx0mnEWYKWn3JSYVihLnkPhk,76 +ddtrace/ext/db.py,sha256=L-gPiwB0qCAvgv43dsQLLFNu-e0smtKUo1o_PvD0MRw,170 +ddtrace/ext/elasticsearch.py,sha256=1NbDiLzJYpzs0KVC7W2q8MsHkcayjFjZwasFP6mRSkI,211 +ddtrace/ext/errors.py,sha256=B7q8UL2nz2xhvhZcTy3crK0-1fVPcPRT8HVFZQD7tgw,949 +ddtrace/ext/git.py,sha256=CxHi28xffo9qW8RKruyt-o-ebW7db2P72utkC8tgq5w,5796 +ddtrace/ext/http.py,sha256=7IycEiu_9n22D8TyGJl7NUKqUi86ZAgdSfSKeG1S87Y,367 +ddtrace/ext/kombu.py,sha256=JqzezXNOH_wlcrQzNAOeM87v4CnJPFXrNeK9hX-UDTk,228 +ddtrace/ext/memcached.py,sha256=Hw1lKBwbo3IfqLrwtcg25IrCE8fRMWRleB5GkFZYpR8,74 +ddtrace/ext/mongo.py,sha256=88NuhD-kD_ojl5RvErmH0eERk1Mi_HrCvqZXSAz-pM4,118 +ddtrace/ext/net.py,sha256=x0b0s_FqdkQhBC_iAAePfDCzzl_tLmhsOuriff57qiY,129 +ddtrace/ext/priority.py,sha256=7b5cwGjhQ0aTjgcpsJQrh5TvCXXU3IjKtTL0QIhuwdg,874 +ddtrace/ext/redis.py,sha256=KBJ8ktQSXpm7PUzI02uHzaqipmNYNbP9VfHLzol5TSE,262 +ddtrace/ext/sql.py,sha256=KpGYyfPtUTYrJAqWGbXPhwimRqd2vmk5JMpkXV9Enqo,950 +ddtrace/ext/system.py,sha256=2URo46I-dmpIArgaVWUXZYKBMt-tLO_bH9kHG97Ygy0,255 +ddtrace/ext/test.py,sha256=1YNhBQWTWjcPuDMeYvLirfaV0tNOl2Ds7I2LqWX_6Vw,900 +ddtrace/filters.py,sha256=DlHUe15T8X9clO59yyxs-ZoUSgDggOs0mJV63gYROnk,2486 +ddtrace/helpers.py,sha256=ML3YXziquFw28Iy9m-6AC1Ug7uZnf5AHBgw9BQPlLok,1752 +ddtrace/http/__init__.py,sha256=mkfA55FuwXk56anWtYvsnqUTLWxHvpOGK36xC_LKpAI,355 +ddtrace/http/__pycache__/__init__.cpython-38.pyc,, +ddtrace/http/__pycache__/headers.cpython-38.pyc,, +ddtrace/http/headers.py,sha256=8wAxw_sMnzcFp11KtuWnN4EhO8p1ZniEVz7B4iyLh5U,931 +ddtrace/internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ddtrace/internal/__pycache__/__init__.cpython-38.pyc,, +ddtrace/internal/__pycache__/agent.cpython-38.pyc,, +ddtrace/internal/__pycache__/atexit.cpython-38.pyc,, +ddtrace/internal/__pycache__/compat.cpython-38.pyc,, +ddtrace/internal/__pycache__/debug.cpython-38.pyc,, +ddtrace/internal/__pycache__/dogstatsd.cpython-38.pyc,, +ddtrace/internal/__pycache__/encoding.cpython-38.pyc,, +ddtrace/internal/__pycache__/forksafe.cpython-38.pyc,, +ddtrace/internal/__pycache__/hostname.cpython-38.pyc,, +ddtrace/internal/__pycache__/http.cpython-38.pyc,, +ddtrace/internal/__pycache__/logger.cpython-38.pyc,, +ddtrace/internal/__pycache__/nogevent.cpython-38.pyc,, +ddtrace/internal/__pycache__/periodic.cpython-38.pyc,, +ddtrace/internal/__pycache__/rate_limiter.cpython-38.pyc,, +ddtrace/internal/__pycache__/service.cpython-38.pyc,, +ddtrace/internal/__pycache__/sma.cpython-38.pyc,, +ddtrace/internal/__pycache__/uds.cpython-38.pyc,, +ddtrace/internal/__pycache__/uwsgi.cpython-38.pyc,, +ddtrace/internal/__pycache__/writer.cpython-38.pyc,, +ddtrace/internal/_encoding.cpython-38-darwin.so,sha256=8xtTnew6b-3RRSVKXEkofGddkuisDCEc6uCQTk_LCV4,227536 +ddtrace/internal/_rand.cpython-38-darwin.so,sha256=anDu2Ba3WjeyFNdW9InTPchlkh-uwTI85Tfld8oep2w,61128 +ddtrace/internal/agent.py,sha256=7PY3TcNtmeaz7Ou1lRBYRTlZXgp7zrPOdHylnlNpbc4,4403 +ddtrace/internal/atexit.py,sha256=xp4vsleQ4iH1vJMOPTr39bPRGO0uTl6z-KgXsS9MtT4,1798 +ddtrace/internal/compat.py,sha256=LQChcevcBJSRc_ZiYIZLMjRgZISq2j1FRbPdftJlj2w,6882 +ddtrace/internal/debug.py,sha256=WJ7vxQqOzUkf_-dzs0GIdInoCaz9UcnMYFHIsTxBUu0,9617 +ddtrace/internal/dogstatsd.py,sha256=Wf9ofudhIjdeIG_PEJSFdwimVTxig5OVdeGYq4o-Pgg,860 +ddtrace/internal/encoding.py,sha256=KgJkoXB7tFyeHUaaBxh6eFLoxodPNoQBwhQrYpYvwnI,2736 +ddtrace/internal/forksafe.py,sha256=lUy7VMMhihi7m4S7NJlZPp8vpSAKLp3CC3aHvTPoE80,3897 +ddtrace/internal/hostname.py,sha256=YPniL_XNPuI4pf_N5XvRR8HNSFtEvX_gw_QY4YKey8g,233 +ddtrace/internal/http.py,sha256=F8XLr90sCfPG_KuAPwppsOdJoSwYzWjJB97Yaon3egw,1103 +ddtrace/internal/logger.py,sha256=8qYG7WObnP3muOCisG2FKFZa7cBTzoECjRbwX4MmVbU,7508 +ddtrace/internal/nogevent.py,sha256=MkgNZJaHw1iuB0pHMZUAFqnIw-ZQL0qBfNYyNtL7cAw,2601 +ddtrace/internal/periodic.py,sha256=CSRZSVL153r9dOvyIKC5SEFLpWL7-UodFK8xAyrKVCU,8379 +ddtrace/internal/processor/__init__.py,sha256=E76wtmhFV_pmQBYf0SxDLChAuJU9NoiewRU9HeS1W1g,1516 +ddtrace/internal/processor/__pycache__/__init__.cpython-38.pyc,, +ddtrace/internal/processor/__pycache__/trace.cpython-38.pyc,, +ddtrace/internal/processor/trace.py,sha256=N4h1KL3X8BClYJ33_Hlh78uKALQYRwrjdSDGt2qr4jw,5509 +ddtrace/internal/rate_limiter.py,sha256=B2kN11YieL4JDMPsp2GTTGO6VNOVlztPm74z2E3n7SM,4726 +ddtrace/internal/runtime/__init__.py,sha256=0pXSt9-wyQAqHhGTP1rUEQ_zc8dK9Iax7GJqE_oMJdE,476 +ddtrace/internal/runtime/__pycache__/__init__.cpython-38.pyc,, +ddtrace/internal/runtime/__pycache__/collector.cpython-38.pyc,, +ddtrace/internal/runtime/__pycache__/constants.cpython-38.pyc,, +ddtrace/internal/runtime/__pycache__/container.cpython-38.pyc,, +ddtrace/internal/runtime/__pycache__/metric_collectors.cpython-38.pyc,, +ddtrace/internal/runtime/__pycache__/runtime_metrics.cpython-38.pyc,, +ddtrace/internal/runtime/__pycache__/tag_collectors.cpython-38.pyc,, +ddtrace/internal/runtime/collector.py,sha256=hf6NC5sNyzkCZMcomEweSCpE_eZqDqN0NStnfNEfvqk,3065 +ddtrace/internal/runtime/constants.py,sha256=bsVI_bvp4YNOCO8oh8JZIegNf8H5nRzMkpLGo4nPayw,1200 +ddtrace/internal/runtime/container.py,sha256=RIyJ_CbuALSLlyDUk7k3-O3fgr9F7UdTZE61sgd3Iiw,3574 +ddtrace/internal/runtime/metric_collectors.py,sha256=Xyzq9KO2-bCOgwbTXx75vgEs5ODuLv1crlLekAJJqBQ,3349 +ddtrace/internal/runtime/runtime_metrics.py,sha256=8d2EHPDrrIoBVy8PqcnA9x-4EZXeg2YXEP5YTi5vTSU,5310 +ddtrace/internal/runtime/tag_collectors.py,sha256=wE0sDJNyEY6mnRhlI4_S8RuQMBMPAoKl4KMM6l90xAc,1742 +ddtrace/internal/service.py,sha256=UNVBANovWev3zcVGJjhqtoFRJx0x0uygT16egz44b8k,2952 +ddtrace/internal/sma.py,sha256=CZJTxxoBnFeMN0_xgn-cms3y3BQfMCBTX0Z_H4N1WVY,1601 +ddtrace/internal/uds.py,sha256=X8WGVq_jLbct2QPBAId3l_OgysTHYttSYnBJ_pO2TGU,834 +ddtrace/internal/uwsgi.py,sha256=hM1z4afz2FiCTZeBIJPl7MrgPDLTonATbZxak0kfXd4,2643 +ddtrace/internal/writer.py,sha256=SdU32_F3IrACSw3UmTWitHOaj3JxGNuXxa_v44WtH38,17974 +ddtrace/monkey.py,sha256=0VgDVfoRDgpT1X2r0F_BBKLxhVmgM05VKiHei6a2aWU,8412 +ddtrace/opentracer/__init__.py,sha256=Z2NreXCsKituj_xkrBrxox6dJME-HY5dAQdoPrT6P4A,121 +ddtrace/opentracer/__pycache__/__init__.cpython-38.pyc,, +ddtrace/opentracer/__pycache__/helpers.cpython-38.pyc,, +ddtrace/opentracer/__pycache__/settings.cpython-38.pyc,, +ddtrace/opentracer/__pycache__/span.cpython-38.pyc,, +ddtrace/opentracer/__pycache__/span_context.cpython-38.pyc,, +ddtrace/opentracer/__pycache__/tags.cpython-38.pyc,, +ddtrace/opentracer/__pycache__/tracer.cpython-38.pyc,, +ddtrace/opentracer/__pycache__/utils.cpython-38.pyc,, +ddtrace/opentracer/helpers.py,sha256=BoojCCOpCc_clAOHcAmCEOvoynxCJGXGSsuJt1JScEM,458 +ddtrace/opentracer/propagation/__init__.py,sha256=dM61Rvq_oyksFQii8bRAUFjm2CZXksIsSf_5fSCjoQw,71 +ddtrace/opentracer/propagation/__pycache__/__init__.cpython-38.pyc,, +ddtrace/opentracer/propagation/__pycache__/binary.cpython-38.pyc,, +ddtrace/opentracer/propagation/__pycache__/http.cpython-38.pyc,, +ddtrace/opentracer/propagation/__pycache__/propagator.cpython-38.pyc,, +ddtrace/opentracer/propagation/__pycache__/text.cpython-38.pyc,, +ddtrace/opentracer/propagation/binary.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ddtrace/opentracer/propagation/http.py,sha256=1XQ6G6hBylaboxedAQXazYKZ7Flkri5Y75jY_C2_ap8,2895 +ddtrace/opentracer/propagation/propagator.py,sha256=Qd9Ktx3GfatKBxNegGetuMvePLLoSZFZ9Eo3GA1lIA8,252 +ddtrace/opentracer/propagation/text.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ddtrace/opentracer/settings.py,sha256=6hMOEWVXteDJLnY3aeM7J9wjtaBKse31VFIc3XNsecU,966 +ddtrace/opentracer/span.py,sha256=IK3hklRO9wbUNJTXXXHAKEt2PYWrnf9BAs9GoHMTe7E,6242 +ddtrace/opentracer/span_context.py,sha256=WtRDu883T1WCpabUwXhINHsxmbwVIZuTG6TmsbwkMLE,2210 +ddtrace/opentracer/tags.py,sha256=I3E0S02ZOV1jxBcdo1guoNhsAMyaeaGlI7LsnnuoSPU,450 +ddtrace/opentracer/tracer.py,sha256=hP0FewbyLddxtxEuk6rRFP6fOyDSJPclc8-9_v2gpw0,15992 +ddtrace/opentracer/utils.py,sha256=BJyYWhPWNUjzDO88R6jD9zabfAP90Vkdyc49YnmI-iY,2156 +ddtrace/pin.py,sha256=7zPcjFGinIGV3SLEJ2sB-8PP3Lcky5wlGiIjKIczAys,7750 +ddtrace/profiling/__init__.py,sha256=55iYcZRG6WJMVRH5cF_iocmCVlPa3HyIVayeqimh9-Y,583 +ddtrace/profiling/__pycache__/__init__.cpython-38.pyc,, +ddtrace/profiling/__pycache__/_traceback.cpython-38.pyc,, +ddtrace/profiling/__pycache__/auto.cpython-38.pyc,, +ddtrace/profiling/__pycache__/event.cpython-38.pyc,, +ddtrace/profiling/__pycache__/profiler.cpython-38.pyc,, +ddtrace/profiling/__pycache__/recorder.cpython-38.pyc,, +ddtrace/profiling/__pycache__/scheduler.cpython-38.pyc,, +ddtrace/profiling/_build.cpython-38-darwin.so,sha256=M1gh7qwZBD7i4CuAsXjmu6JiNcfAq6NCBJSMCXXQQFA,39968 +ddtrace/profiling/_traceback.py,sha256=gtTV5xW0HKO2azPwCNlx2PdJ0KnM4TcKo7EeJm8cSrU,111 +ddtrace/profiling/auto.py,sha256=-gFCU-SK-b4PSWE8OY8BSWG6P2swCyS2Ca6cfhB9kGA,164 +ddtrace/profiling/bootstrap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ddtrace/profiling/bootstrap/__pycache__/__init__.cpython-38.pyc,, +ddtrace/profiling/bootstrap/__pycache__/sitecustomize.cpython-38.pyc,, +ddtrace/profiling/bootstrap/sitecustomize.py,sha256=zSmUmKEOjy4hq1Jiw3yIrhGUQRCl7W3LAHLlLbRbvU0,431 +ddtrace/profiling/collector/__init__.py,sha256=6cFnJVq2ydnIDDI3KXot-_tzs78eoy-W3tT9NOOnPvU,2093 +ddtrace/profiling/collector/__pycache__/__init__.cpython-38.pyc,, +ddtrace/profiling/collector/__pycache__/memalloc.cpython-38.pyc,, +ddtrace/profiling/collector/__pycache__/threading.cpython-38.pyc,, +ddtrace/profiling/collector/_memalloc.cpython-38-darwin.so,sha256=I6ClVJ67FiP526O19ASZBrDMYDlt8H_xk1fNGBZGJA8,40432 +ddtrace/profiling/collector/_task.cpython-38-darwin.so,sha256=Po2VjaybBuTLF-AkgbixStkgs4Uqsba99ljK_Qn316w,73664 +ddtrace/profiling/collector/_threading.cpython-38-darwin.so,sha256=a1xG0R6WLOrCvaUcYPcwEQp2Qi54Dl2riT2nQMZCH3c,45056 +ddtrace/profiling/collector/_traceback.cpython-38-darwin.so,sha256=vms1Evg5uMgg6RH_uk6WxD9OODGPmtSNSaD-esZqTiM,43288 +ddtrace/profiling/collector/memalloc.py,sha256=-YQOcLvOiGxOrYMYrXOn85rBciFCtMtT1cPKMsFdTXU,5717 +ddtrace/profiling/collector/stack.cpython-38-darwin.so,sha256=qTNKxolqcd6n77qzaN3fmv5gEMHZGXCQVefYELRa_g4,271240 +ddtrace/profiling/collector/threading.py,sha256=hAkYVcGgaN5iVcwIwLURKBKIwgtUU0EaWwiJrkzt8EI,7222 +ddtrace/profiling/event.py,sha256=Gd61FZkRzdQMQredSkZSFdjgxCyYJwsRabTDfgvN-tY,1756 +ddtrace/profiling/exporter/__init__.py,sha256=i_bSYLlRxMaCQG1UfinGviQFAHqN3ZUd8UrUzSYXs-I,653 +ddtrace/profiling/exporter/__pycache__/__init__.cpython-38.pyc,, +ddtrace/profiling/exporter/__pycache__/file.cpython-38.pyc,, +ddtrace/profiling/exporter/__pycache__/http.cpython-38.pyc,, +ddtrace/profiling/exporter/__pycache__/pprof_pb2.cpython-38.pyc,, +ddtrace/profiling/exporter/__pycache__/pprof_pre312_pb2.cpython-38.pyc,, +ddtrace/profiling/exporter/file.py,sha256=g49Q0Wcoyv968z-opIcOp6RvnM8UjEYSC1_EAYpJBn0,1000 +ddtrace/profiling/exporter/http.py,sha256=Dywj14SZb5LMXK9PgnI3k3bcW0P57Awk1pzh1vLQjlo,7292 +ddtrace/profiling/exporter/pprof.cpython-38-darwin.so,sha256=HzFm7rIEvV-Eb2VoLXjxOvGNMYmMtE5xfXMZxAQDIoI,457080 +ddtrace/profiling/exporter/pprof_pb2.py,sha256=K-waNza388ffg2MtAdxZZRLkhyQTt1SxIZVCmBPTMWk,28618 +ddtrace/profiling/exporter/pprof_pre312_pb2.py,sha256=lj4Su40oadwotRGrDGW8lG774Vs7d-Pu7AsEt_hzSuI,24821 +ddtrace/profiling/profiler.py,sha256=uc-iesyBjLwYOceBfrfe_HSooWaCVqIE8mQssB_F3_U,9632 +ddtrace/profiling/recorder.py,sha256=n_tSEab9QILn0dMSA1OvawVO6dJ5DXBRd_NUwqY_3wI,2930 +ddtrace/profiling/scheduler.py,sha256=YpqcH-2OJ4F9DNdRACLdL8TmxoCl3UyuN-XaQdD4MXI,2395 +ddtrace/propagation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ddtrace/propagation/__pycache__/__init__.cpython-38.pyc,, +ddtrace/propagation/__pycache__/http.cpython-38.pyc,, +ddtrace/propagation/__pycache__/utils.cpython-38.pyc,, +ddtrace/propagation/http.py,sha256=eebpozo9uF5Xt8-gXOt-ubL_MpukHrBfPGJXZ0AVxS4,5875 +ddtrace/propagation/utils.py,sha256=6_wd3_Zd9rHWvhMlV7bgs51Abja0HrP2-yDnMcqsj1Y,954 +ddtrace/provider.py,sha256=llmCp7s4NVvi23eHqiW_UNbGf7b59pLMJdSdiALAcg8,4439 +ddtrace/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ddtrace/runtime/__init__.py,sha256=AR66LSao3DEPCaWd_N6IG_0ndUIVtOEy7VQPHcIMucQ,1804 +ddtrace/runtime/__pycache__/__init__.cpython-38.pyc,, +ddtrace/sampler.py,sha256=xu8eDZWk-VZPk8ULc71tlXQaNz66IBCcnypQvi7jeoo,16210 +ddtrace/settings/__init__.py,sha256=K7w4Jo7H8WFNSUlCP_cTwhHHG4C-oO8LlC7yBpYb4x4,319 +ddtrace/settings/__pycache__/__init__.cpython-38.pyc,, +ddtrace/settings/__pycache__/config.cpython-38.pyc,, +ddtrace/settings/__pycache__/exceptions.cpython-38.pyc,, +ddtrace/settings/__pycache__/http.cpython-38.pyc,, +ddtrace/settings/__pycache__/integration.cpython-38.pyc,, +ddtrace/settings/config.py,sha256=dYSppcV9dCi02HzuRpclGf6ZmzDt4KGto6uWX_HFyys,9544 +ddtrace/settings/exceptions.py,sha256=w0BejP6qaqnGpwCBsn8oJ65gEmTg1ESDmvDrKS_YRDo,163 +ddtrace/settings/http.py,sha256=u7bR8cVcRVA1MYy4ta9hArAir6bHt_Px4fu6lH89WmI,2850 +ddtrace/settings/integration.py,sha256=4dL7bf883tL4bQqINM36T1wM9AkAf0VIbcXb8IUw374,5203 +ddtrace/span.py,sha256=NS8lSAgFLYEEPNivJy0HhUT0ky0AE7ozl-n4AYc2Z-I,17733 +ddtrace/tracer.py,sha256=G8igS2L8AOivxgdDvvPEWkifyJNZv-Ad3JwYQK9ucmU,40027 +ddtrace/util.py,sha256=qsoEPYeFesf2Oi0R-fhjUFCx-WXEaKppQd-lOUn3z-Y,557 +ddtrace/utils/__init__.py,sha256=lMXYoU9XYL3uB6QIfVt_v-yHV1Uh_55PtVkMRFwvrc8,1455 +ddtrace/utils/__pycache__/__init__.cpython-38.pyc,, +ddtrace/utils/__pycache__/attr.cpython-38.pyc,, +ddtrace/utils/__pycache__/attrdict.cpython-38.pyc,, +ddtrace/utils/__pycache__/cache.cpython-38.pyc,, +ddtrace/utils/__pycache__/config.cpython-38.pyc,, +ddtrace/utils/__pycache__/deprecation.cpython-38.pyc,, +ddtrace/utils/__pycache__/formats.cpython-38.pyc,, +ddtrace/utils/__pycache__/http.cpython-38.pyc,, +ddtrace/utils/__pycache__/importlib.cpython-38.pyc,, +ddtrace/utils/__pycache__/time.cpython-38.pyc,, +ddtrace/utils/__pycache__/version.cpython-38.pyc,, +ddtrace/utils/__pycache__/wrappers.cpython-38.pyc,, +ddtrace/utils/attr.py,sha256=8ch3kfteqHbO1Xvx9d_NuUl_ZXg_NVst6Mi-F4BEnOY,352 +ddtrace/utils/attrdict.py,sha256=0UNAIOd2uiWZoFfkYInPzXy8Syboa1swwnFRXRYZV4Y,1168 +ddtrace/utils/cache.py,sha256=EzwNnOcMMQvv5EbV-tCkCSK24u7xQ2Y-flWfde-2xmM,2348 +ddtrace/utils/config.py,sha256=TH5zocgnIjK7qTDRrstzg1NmUNfSsf1THOIkRbxTo_Q,433 +ddtrace/utils/deprecation.py,sha256=3Cu04XWo-NOXKehHx7pDWDnyNd63GfkcaOJyjOgDqdQ,3619 +ddtrace/utils/formats.py,sha256=hbEKur4TEbBdoQe1eLr-fLzr3sKxqQp9dzwdyjakt84,4583 +ddtrace/utils/http.py,sha256=jbhE6CSwi1B02-WoLL96AbuFJ5SG0aUEYg2u62OOJoM,787 +ddtrace/utils/importlib.py,sha256=CAEockrG7xLz35BJ0Mtlva8YTAk2sDJ7VI9i6MPQ2D0,1343 +ddtrace/utils/time.py,sha256=YfVjqlsM19bdbqPJ8MMzMTz2z0FNSLpD8uNRM6C-GLI,2235 +ddtrace/utils/version.py,sha256=EdME5hpsAlyPiqP7P2gBUjOyXVkzxC3KKk0NrU4hUW0,1570 +ddtrace/utils/wrappers.py,sha256=9yDqDerBKY8xGTL5H1spGdlBUNqBiRyUZJNiFdAsDro,2984 +ddtrace/vendor/__init__.py,sha256=GqCDtlK1rzjZ4vG1bJtvUGYqaH-LKMCqURI9BchMVYw,2519 +ddtrace/vendor/__pycache__/__init__.cpython-38.pyc,, +ddtrace/vendor/contextvars/__init__.py,sha256=TRvgsEausfPDhHwEBJ-z2tLSMWRC6E_088v4ODkcnXg,3898 +ddtrace/vendor/contextvars/__pycache__/__init__.cpython-38.pyc,, +ddtrace/vendor/debtcollector/__init__.py,sha256=Q8OE09M7ZpXatj2Ft47FVL_ClA5KY5upavDWXg1DKtE,2175 +ddtrace/vendor/debtcollector/__pycache__/__init__.cpython-38.pyc,, +ddtrace/vendor/debtcollector/__pycache__/_utils.cpython-38.pyc,, +ddtrace/vendor/debtcollector/__pycache__/moves.cpython-38.pyc,, +ddtrace/vendor/debtcollector/__pycache__/removals.cpython-38.pyc,, +ddtrace/vendor/debtcollector/__pycache__/renames.cpython-38.pyc,, +ddtrace/vendor/debtcollector/__pycache__/updating.cpython-38.pyc,, +ddtrace/vendor/debtcollector/_utils.py,sha256=OXhJruEi9X2H0EaVIcNkByltmf2VKkczf2IAfOB71SE,6346 +ddtrace/vendor/debtcollector/moves.py,sha256=tapV2utvk2OtYcM_joe_l3Yg3XuTtEZkbeVT-tfKcCQ,8421 +ddtrace/vendor/debtcollector/removals.py,sha256=0q5MvpcXKP_W1Jx6Qp4URBwYqYc3q_rsJRgKT3VW-Kw,13890 +ddtrace/vendor/debtcollector/renames.py,sha256=Lok-0KYHlVCCho9vkFsFPFI6VtgUzk0z5NdUyEUH9wA,1715 +ddtrace/vendor/debtcollector/updating.py,sha256=2x54f0_UKhs_SsGHWzt0zpEFq6M0_RzmC7O42ZXaFLo,2426 +ddtrace/vendor/dogstatsd/__init__.py,sha256=AD8b1g1ld26XDE1NgUtknfyv0qrI931nLa0ohpwTda0,277 +ddtrace/vendor/dogstatsd/__pycache__/__init__.cpython-38.pyc,, +ddtrace/vendor/dogstatsd/__pycache__/base.cpython-38.pyc,, +ddtrace/vendor/dogstatsd/__pycache__/compat.cpython-38.pyc,, +ddtrace/vendor/dogstatsd/__pycache__/context.cpython-38.pyc,, +ddtrace/vendor/dogstatsd/__pycache__/context_async.cpython-38.pyc,, +ddtrace/vendor/dogstatsd/__pycache__/format.cpython-38.pyc,, +ddtrace/vendor/dogstatsd/__pycache__/route.cpython-38.pyc,, +ddtrace/vendor/dogstatsd/base.py,sha256=gx8dL6iYwYmkmzSCfV__jcUvhIeyl2Ji3m9Mt1M72js,23200 +ddtrace/vendor/dogstatsd/compat.py,sha256=_WXnttqiPGQ9r2qOWfDXfJvv82nIiUUPQg1huCfjll8,995 +ddtrace/vendor/dogstatsd/context.py,sha256=DcGU8obtgG9uA8jhwKssY0wvse6PZYPEWxfWddoL-7Q,2722 +ddtrace/vendor/dogstatsd/context_async.py,sha256=aLj58WPZjyhGRn2y6IXaC3nV3ustPJJkA7eAIceSehM,1473 +ddtrace/vendor/dogstatsd/format.py,sha256=maACZlLz8DuSv1sNyhQQQtgswyLPiR98HiHU6cwhxRE,1025 +ddtrace/vendor/dogstatsd/route.py,sha256=3jR6_z_Z-cAkEVNAri7n8GSoZzw4wW8cwjy_DDsNZVI,1265 +ddtrace/vendor/monotonic/__init__.py,sha256=1wJOetpAPQUteaP7IxAelyChpkITsxZf-eV4V2bTHrA,7117 +ddtrace/vendor/monotonic/__pycache__/__init__.cpython-38.pyc,, +ddtrace/vendor/psutil/__init__.py,sha256=zLnjfXMB3aOXBkgwVbiaTzFYKvDpY3bRBS12BryUMn8,90581 +ddtrace/vendor/psutil/__pycache__/__init__.cpython-38.pyc,, +ddtrace/vendor/psutil/__pycache__/_common.cpython-38.pyc,, +ddtrace/vendor/psutil/__pycache__/_compat.cpython-38.pyc,, +ddtrace/vendor/psutil/__pycache__/_psaix.cpython-38.pyc,, +ddtrace/vendor/psutil/__pycache__/_psbsd.cpython-38.pyc,, +ddtrace/vendor/psutil/__pycache__/_pslinux.cpython-38.pyc,, +ddtrace/vendor/psutil/__pycache__/_psosx.cpython-38.pyc,, +ddtrace/vendor/psutil/__pycache__/_psposix.cpython-38.pyc,, +ddtrace/vendor/psutil/__pycache__/_pssunos.cpython-38.pyc,, +ddtrace/vendor/psutil/__pycache__/_pswindows.cpython-38.pyc,, +ddtrace/vendor/psutil/__pycache__/setup.cpython-38.pyc,, +ddtrace/vendor/psutil/_common.py,sha256=eA2kZtQPvlmMpKw_Fz3lswXSg35tthoM6OAb2UhdNNQ,20224 +ddtrace/vendor/psutil/_compat.py,sha256=c9jBW_7ZcDyl562gIeIh53drZ-oUVLPobOnuAJRhY2w,11191 +ddtrace/vendor/psutil/_psaix.py,sha256=IDY57Ybv5k4eSKH50lD7EJ9slfRoPJ-SLKyniXQFvkw,18564 +ddtrace/vendor/psutil/_psbsd.py,sha256=kvDbgjD38KtIZDhsTNbmMowvjSmKs701D-aYr_5wPQE,30566 +ddtrace/vendor/psutil/_pslinux.py,sha256=N-p3vkd-QG9132CIihCIZ47mPa_vwBWkHZf6x3pI6Xg,79839 +ddtrace/vendor/psutil/_psosx.py,sha256=JbNktzY5i5xQJTWNWdWbFSRPBZneb4_34Pm6GKyDiZs,17196 +ddtrace/vendor/psutil/_psposix.py,sha256=sQajYsNSDFV0HqN3GFf7Rvh8vu9eQLbzMpD2eqgakVk,6159 +ddtrace/vendor/psutil/_pssunos.py,sha256=ZayYw299DPsmA8TzA7UpuFuigW49OC0KrdrU4A1hOlY,25109 +ddtrace/vendor/psutil/_psutil_osx.cpython-38-darwin.so,sha256=XFYhxHoCXbeT0LLtD1G1DUSCkof8Dju0T1VfshX_0UE,65232 +ddtrace/vendor/psutil/_psutil_posix.cpython-38-darwin.so,sha256=pA0PNC2zOw0BhmyrPexGb1NSjY9SscD64zycfgZONyc,65232 +ddtrace/vendor/psutil/_pswindows.py,sha256=A2mlRzUvPxPrj8LETN25MAn6fRcLp12L-RP7bwP2TKM,37400 +ddtrace/vendor/psutil/setup.py,sha256=Ze5fZ_gj3Y6e4i3Wb446CgVdiICXpfLYkZVvlllusqo,7931 +ddtrace/vendor/wrapt/__init__.py,sha256=FS1Cu1Ai6qjtdTDMKVtgCTb0gW4mPOlM2111JlqpFq4,658 +ddtrace/vendor/wrapt/__pycache__/__init__.cpython-38.pyc,, +ddtrace/vendor/wrapt/__pycache__/decorators.cpython-38.pyc,, +ddtrace/vendor/wrapt/__pycache__/importer.cpython-38.pyc,, +ddtrace/vendor/wrapt/__pycache__/setup.cpython-38.pyc,, +ddtrace/vendor/wrapt/__pycache__/wrappers.cpython-38.pyc,, +ddtrace/vendor/wrapt/_wrappers.cpython-38-darwin.so,sha256=ONxK3mm-v9g3I9n0neBbnBcHWryS4BOwmz-eg7jxeRw,74152 +ddtrace/vendor/wrapt/decorators.py,sha256=mUjuO9o-Gx0QI9ycp-N6txBv9cDwUf_7n2HlgtOmBGs,20247 +ddtrace/vendor/wrapt/importer.py,sha256=H-d3dCnVxGzz6UTrH4Xncqet5yPX9k-Lo5upa9hlQsc,7896 +ddtrace/vendor/wrapt/setup.py,sha256=CF2p_6VhgEGASbK2JH_MARGMt3GHe6uADKRqu573QY0,191 +ddtrace/vendor/wrapt/wrappers.py,sha256=IAcZRdrc38Bd-vq7Cekq65kAvoO1DXSzjISDQ1Hm6I8,33768 +ddtrace/version.py,sha256=baS8Td1e5xpdw4FfJmWDP8dBZ8xj6CIKk1RtyMLJ-eE,458 +ddtrace_gevent_check.py,sha256=hxO0Ljq9o8RBK1zMw5N34P73YXIG3LnjBcNqq6Oeho0,447 diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/WHEEL b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/WHEEL new file mode 100644 index 000000000..505a8e600 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: false +Tag: cp38-cp38-macosx_10_9_x86_64 + diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/entry_points.txt b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/entry_points.txt new file mode 100644 index 000000000..c80af2699 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/entry_points.txt @@ -0,0 +1,9 @@ +[console_scripts] +ddtrace-run = ddtrace.commands.ddtrace_run:main + +[gevent.plugins.monkey.did_patch_all] +ddtrace_gevent_check = ddtrace_gevent_check:gevent_patch_all + +[pytest11] +ddtrace = ddtrace.contrib.pytest.plugin + diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/top_level.txt b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/top_level.txt new file mode 100644 index 000000000..9580c4763 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace-0.55.4.dist-info/top_level.txt @@ -0,0 +1,3 @@ +benchmarks +ddtrace +ddtrace_gevent_check diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__init__.py new file mode 100644 index 000000000..789dd6efc --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__init__.py @@ -0,0 +1,34 @@ +from .monkey import patch # noqa: E402 +from .monkey import patch_all +from .pin import Pin # noqa: E402 +from .settings import _config as config # noqa: E402 +from .span import Span # noqa: E402 +from .tracer import Tracer # noqa: E402 +from .utils.deprecation import deprecated # noqa: E402 +from .version import get_version + + +__version__ = get_version() + +# a global tracer instance with integration settings +tracer = Tracer() + +__all__ = [ + "patch", + "patch_all", + "Pin", + "Span", + "tracer", + "Tracer", + "config", +] + + +@deprecated("This method will be removed altogether", "1.0.0") +def install_excepthook(): + """Install a hook that intercepts unhandled exception and send metrics about them.""" + + +@deprecated("This method will be removed altogether", "1.0.0") +def uninstall_excepthook(): + """Uninstall the global tracer except hook.""" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..e22184162 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/_hooks.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/_hooks.cpython-38.pyc new file mode 100644 index 000000000..ad9ea4ffe Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/_hooks.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/_version.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/_version.cpython-38.pyc new file mode 100644 index 000000000..ad441a466 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/_version.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/compat.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/compat.cpython-38.pyc new file mode 100644 index 000000000..159ed0941 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/compat.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/constants.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/constants.cpython-38.pyc new file mode 100644 index 000000000..0e4c06928 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/constants.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/context.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/context.cpython-38.pyc new file mode 100644 index 000000000..0942cced0 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/context.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/encoding.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/encoding.cpython-38.pyc new file mode 100644 index 000000000..1920856ad Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/encoding.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/filters.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/filters.cpython-38.pyc new file mode 100644 index 000000000..d776c6940 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/filters.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/helpers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/helpers.cpython-38.pyc new file mode 100644 index 000000000..0f09e90fe Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/helpers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/monkey.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/monkey.cpython-38.pyc new file mode 100644 index 000000000..a7dc09671 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/monkey.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/pin.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/pin.cpython-38.pyc new file mode 100644 index 000000000..b3e12d404 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/pin.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/provider.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/provider.cpython-38.pyc new file mode 100644 index 000000000..2b8b71f6b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/provider.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/sampler.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/sampler.cpython-38.pyc new file mode 100644 index 000000000..9e03e3267 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/sampler.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/span.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/span.cpython-38.pyc new file mode 100644 index 000000000..cda3b851e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/span.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/tracer.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/tracer.cpython-38.pyc new file mode 100644 index 000000000..54dc0ddbb Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/tracer.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/util.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/util.cpython-38.pyc new file mode 100644 index 000000000..0341dbd9d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/util.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/version.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/version.cpython-38.pyc new file mode 100644 index 000000000..cde48bef2 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/__pycache__/version.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/_hooks.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/_hooks.py new file mode 100644 index 000000000..3283fe4df --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/_hooks.py @@ -0,0 +1,133 @@ +import collections +from copy import deepcopy +from typing import Any +from typing import Callable +from typing import DefaultDict +from typing import Optional +from typing import Set + +import attr + +from .internal.logger import get_logger + + +log = get_logger(__name__) + + +@attr.s(slots=True) +class Hooks(object): + """ + Hooks configuration object is used for registering and calling hook functions + + Example:: + + @config.falcon.hooks.on('request') + def on_request(span, request, response): + pass + """ + + _hooks = attr.ib(init=False, factory=lambda: collections.defaultdict(set), type=DefaultDict[str, Set]) + + def __deepcopy__(self, memodict=None): + hooks = Hooks() + hooks._hooks = deepcopy(self._hooks, memodict) + return hooks + + def register( + self, + hook, # type: Any + func=None, # type: Optional[Callable] + ): + # type: (...) -> Optional[Callable[..., Any]] + """ + Function used to register a hook for the provided name. + + Example:: + + def on_request(span, request, response): + pass + + config.falcon.hooks.register('request', on_request) + + + If no function is provided then a decorator is returned:: + + @config.falcon.hooks.register('request') + def on_request(span, request, response): + pass + + :param hook: The name of the hook to register the function for + :type hook: object + :param func: The function to register, or ``None`` if a decorator should be returned + :type func: function, None + :returns: Either a function decorator if ``func is None``, otherwise ``None`` + :rtype: function, None + """ + # If they didn't provide a function, then return a decorator + if not func: + + def wrapper(func): + self.register(hook, func) + return func + + return wrapper + self._hooks[hook].add(func) + return None + + # Provide shorthand `on` method for `register` + # >>> @config.falcon.hooks.on('request') + # def on_request(span, request, response): + # pass + on = register + + def deregister( + self, + hook, # type: Any + func, # type: Callable + ): + # type: (...) -> None + """ + Function to deregister a function from a hook it was registered under + + Example:: + + @config.falcon.hooks.on('request') + def on_request(span, request, response): + pass + + config.falcon.hooks.deregister('request', on_request) + + :param hook: The name of the hook to register the function for + :type hook: object + :param func: Function hook to register + :type func: function + """ + if hook in self._hooks: + try: + self._hooks[hook].remove(func) + except KeyError: + pass + + def emit( + self, + hook, # type: Any + *args, # type: Any + **kwargs # type: Any + ): + # type: (...) -> None + """ + Function used to call registered hook functions. + + :param hook: The hook to call functions for + :type hook: str + :param args: Positional arguments to pass to the hook functions + :type args: list + :param kwargs: Keyword arguments to pass to the hook functions + :type kwargs: dict + """ + # Call registered hooks + for func in self._hooks.get(hook, ()): + try: + func(*args, **kwargs) + except Exception: + log.error("Failed to run hook %s function %s", hook, func, exc_info=True) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/_version.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/_version.py new file mode 100644 index 000000000..a8850ee3b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/_version.py @@ -0,0 +1,5 @@ +# coding: utf-8 +# file generated by setuptools_scm +# don't change, don't track in version control +version = '0.55.4' +version_tuple = (0, 55, 4) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/bootstrap/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/bootstrap/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/bootstrap/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/bootstrap/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..df5ae6465 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/bootstrap/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/bootstrap/__pycache__/sitecustomize.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/bootstrap/__pycache__/sitecustomize.cpython-38.pyc new file mode 100644 index 000000000..b00b34825 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/bootstrap/__pycache__/sitecustomize.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/bootstrap/sitecustomize.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/bootstrap/sitecustomize.py new file mode 100644 index 000000000..b37f1a86b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/bootstrap/sitecustomize.py @@ -0,0 +1,163 @@ +""" +Bootstrapping code that is run when using the `ddtrace-run` Python entrypoint +Add all monkey-patching that needs to run by default here +""" +import logging +import os +import sys +from typing import Any +from typing import Dict + + +# Perform gevent patching as early as possible in the application before +# importing more of the library internals. +if os.environ.get("DD_GEVENT_PATCH_ALL", "false").lower() in ("true", "1"): + import gevent.monkey + + gevent.monkey.patch_all() + + +from ddtrace import config # noqa +from ddtrace import constants +from ddtrace.internal.logger import get_logger # noqa +from ddtrace.internal.runtime.runtime_metrics import RuntimeWorker +from ddtrace.tracer import DD_LOG_FORMAT # noqa +from ddtrace.tracer import debug_mode +from ddtrace.utils.formats import asbool # noqa +from ddtrace.utils.formats import get_env +from ddtrace.utils.formats import parse_tags_str + + +if config.logs_injection: + # immediately patch logging if trace id injected + from ddtrace import patch + + patch(logging=True) + + +# DEV: Once basicConfig is called here, future calls to it cannot be used to +# change the formatter since it applies the formatter to the root handler only +# upon initializing it the first time. +# See https://github.com/python/cpython/blob/112e4afd582515fcdcc0cde5012a4866e5cfda12/Lib/logging/__init__.py#L1550 +# Debug mode from the tracer will do a basicConfig so only need to do this otherwise +call_basic_config = asbool(os.environ.get("DD_CALL_BASIC_CONFIG", "true")) +if not debug_mode and call_basic_config: + if config.logs_injection: + logging.basicConfig(format=DD_LOG_FORMAT) + else: + logging.basicConfig() + +log = get_logger(__name__) + +EXTRA_PATCHED_MODULES = { + "bottle": True, + "django": True, + "falcon": True, + "flask": True, + "pylons": True, + "pyramid": True, +} + + +def update_patched_modules(): + modules_to_patch = get_env("patch", "modules") + if not modules_to_patch: + return + + modules = parse_tags_str(modules_to_patch) + for module, should_patch in modules.items(): + EXTRA_PATCHED_MODULES[module] = asbool(should_patch) + + +try: + from ddtrace import tracer + + # Respect DATADOG_* environment variables in global tracer configuration but add deprecation warning + # correct prefix should be DD_* + + dd_hostname = get_env("trace", "agent", "hostname") + hostname = os.environ.get("DD_AGENT_HOST", dd_hostname) + port = get_env("trace", "agent", "port") + priority_sampling = get_env("priority", "sampling") + profiling = asbool(os.environ.get("DD_PROFILING_ENABLED", False)) + + if profiling: + import ddtrace.profiling.auto # noqa: F401 + + if asbool(get_env("runtime_metrics", "enabled")): + RuntimeWorker.enable() + + opts = {} # type: Dict[str, Any] + + dd_trace_enabled = get_env("trace", "enabled", default=True) + if asbool(dd_trace_enabled): + trace_enabled = True + else: + trace_enabled = False + opts["enabled"] = False + + if hostname: + opts["hostname"] = hostname + if port: + opts["port"] = int(port) + if priority_sampling: + opts["priority_sampling"] = asbool(priority_sampling) + + tracer.configure(**opts) + + if trace_enabled: + update_patched_modules() + from ddtrace import patch_all + + patch_all(**EXTRA_PATCHED_MODULES) + + dd_env = get_env("env") + if dd_env: + tracer.set_tags({constants.ENV_KEY: dd_env}) + + if "DD_TRACE_GLOBAL_TAGS" in os.environ: + env_tags = os.getenv("DD_TRACE_GLOBAL_TAGS") + tracer.set_tags(parse_tags_str(env_tags)) + + # Check for and import any sitecustomize that would have normally been used + # had ddtrace-run not been used. + bootstrap_dir = os.path.dirname(__file__) + if bootstrap_dir in sys.path: + index = sys.path.index(bootstrap_dir) + del sys.path[index] + + # NOTE: this reference to the module is crucial in Python 2. + # Without it the current module gets gc'd and all subsequent references + # will be `None`. + ddtrace_sitecustomize = sys.modules["sitecustomize"] + del sys.modules["sitecustomize"] + try: + import sitecustomize # noqa + except ImportError: + # If an additional sitecustomize is not found then put the ddtrace + # sitecustomize back. + log.debug("additional sitecustomize not found") + sys.modules["sitecustomize"] = ddtrace_sitecustomize + else: + log.debug("additional sitecustomize found in: %s", sys.path) + finally: + # Always reinsert the ddtrace bootstrap directory to the path so + # that introspection and debugging the application makes sense. + # Note that this does not interfere with imports since a user + # sitecustomize, if it exists, will be imported. + sys.path.insert(index, bootstrap_dir) + else: + try: + import sitecustomize # noqa + except ImportError: + log.debug("additional sitecustomize not found") + else: + log.debug("additional sitecustomize found in: %s", sys.path) + + # Loading status used in tests to detect if the `sitecustomize` has been + # properly loaded without exceptions. This must be the last action in the module + # when the execution ends with a success. + loaded = True +except Exception: + loaded = False + log.warning("error configuring Datadog tracing", exc_info=True) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/commands/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/commands/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/commands/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..e29ac2204 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/commands/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/commands/__pycache__/ddtrace_run.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/commands/__pycache__/ddtrace_run.cpython-38.pyc new file mode 100644 index 000000000..03303eb68 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/commands/__pycache__/ddtrace_run.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/commands/ddtrace_run.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/commands/ddtrace_run.py new file mode 100644 index 000000000..14ac733da --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/commands/ddtrace_run.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +import argparse +from distutils import spawn +import logging +import os +import sys + +import ddtrace +from ddtrace.internal.compat import PY2 +from ddtrace.utils.formats import asbool +from ddtrace.utils.formats import get_env + + +if PY2: + # Python 2 does not have PermissionError but Python 3 does. + PermissionError = None # noqa: A001 + + +# Do not use `ddtrace.internal.logger.get_logger` here +# DEV: It isn't really necessary to use `DDLogger` here so we want to +# defer importing `ddtrace` until we actually need it. +# As well, no actual rate limiting would apply here since we only +# have a few logged lines +log = logging.getLogger(__name__) + +USAGE = """ +Execute the given Python command after configuring it to emit Datadog traces +and profiles. + + +Examples +ddtrace-run python app.py +ddtrace-run gunicorn myproject.wsgi +""" + + +def _add_bootstrap_to_pythonpath(bootstrap_dir): + """ + Add our bootstrap directory to the head of $PYTHONPATH to ensure + it is loaded before program code + """ + python_path = os.environ.get("PYTHONPATH", "") + + if python_path: + new_path = "%s%s%s" % (bootstrap_dir, os.path.pathsep, os.environ["PYTHONPATH"]) + os.environ["PYTHONPATH"] = new_path + else: + os.environ["PYTHONPATH"] = bootstrap_dir + + +def main(): + parser = argparse.ArgumentParser( + description=USAGE, + prog="ddtrace-run", + usage="ddtrace-run ", + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument("command", nargs=argparse.REMAINDER, type=str, help="Command string to execute.") + parser.add_argument("-d", "--debug", help="enable debug mode (disabled by default)", action="store_true") + parser.add_argument( + "-i", + "--info", + help=( + "print library info useful for debugging. Only reflects configurations made via environment " + "variables, not those made in code." + ), + action="store_true", + ) + parser.add_argument("-p", "--profiling", help="enable profiling (disabled by default)", action="store_true") + parser.add_argument("-v", "--version", action="version", version="%(prog)s " + ddtrace.__version__) + parser.add_argument("-nc", "--colorless", help="print output of command without color", action="store_true") + args = parser.parse_args() + + if args.profiling: + os.environ["DD_PROFILING_ENABLED"] = "true" + + debug_mode = args.debug or asbool(get_env("trace", "debug", default=False)) + + if debug_mode: + logging.basicConfig(level=logging.DEBUG) + os.environ["DD_TRACE_DEBUG"] = "true" + + if args.info: + # Inline imports for performance. + from ddtrace.internal.debug import pretty_collect + + print(pretty_collect(ddtrace.tracer, color=not args.colorless)) + sys.exit(0) + + root_dir = os.path.dirname(ddtrace.__file__) + log.debug("ddtrace root: %s", root_dir) + + bootstrap_dir = os.path.join(root_dir, "bootstrap") + log.debug("ddtrace bootstrap: %s", bootstrap_dir) + + _add_bootstrap_to_pythonpath(bootstrap_dir) + log.debug("PYTHONPATH: %s", os.environ["PYTHONPATH"]) + log.debug("sys.path: %s", sys.path) + + if not args.command: + parser.print_help() + sys.exit(1) + + # Find the executable path + executable = spawn.find_executable(args.command[0]) + if not executable: + print("ddtrace-run: failed to find executable '%s'.\n" % args.command[0]) + parser.print_usage() + sys.exit(1) + + log.debug("program executable: %s", executable) + + if os.path.basename(executable) == "uwsgi": + print( + ( + "ddtrace-run has known compatibility issues with uWSGI where the " + "tracer is not started properly in uWSGI workers which can cause " + "broken behavior. It is recommended you remove ddtrace-run and " + "update your uWSGI configuration following " + "https://ddtrace.readthedocs.io/en/stable/advanced_usage.html#uwsgi." + ) + ) + + try: + os.execl(executable, executable, *args.command[1:]) + except PermissionError: + print("ddtrace-run: permission error while launching '%s'" % executable) + print("Did you mean `ddtrace-run python %s`?" % executable) + sys.exit(1) + except Exception: + print("ddtrace-run: error launching '%s'" % executable) + raise + + sys.exit(0) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/compat.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/compat.py new file mode 100644 index 000000000..e2f4c222e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/compat.py @@ -0,0 +1,80 @@ +from ddtrace.utils.deprecation import deprecation + +from .internal.compat import CONTEXTVARS_IS_AVAILABLE +from .internal.compat import NumericType +from .internal.compat import PY2 +from .internal.compat import PY3 +from .internal.compat import PYTHON_INTERPRETER +from .internal.compat import PYTHON_VERSION +from .internal.compat import PYTHON_VERSION_INFO +from .internal.compat import Queue +from .internal.compat import StringIO +from .internal.compat import binary_type +from .internal.compat import contextvars +from .internal.compat import ensure_text +from .internal.compat import get_connection_response +from .internal.compat import getrandbits +from .internal.compat import httplib +from .internal.compat import is_integer +from .internal.compat import iscoroutinefunction +from .internal.compat import iteritems +from .internal.compat import main_thread +from .internal.compat import make_async_decorator +from .internal.compat import monotonic +from .internal.compat import monotonic_ns +from .internal.compat import msgpack_type +from .internal.compat import numeric_types +from .internal.compat import parse +from .internal.compat import pattern_type +from .internal.compat import process_time_ns +from .internal.compat import reload_module +from .internal.compat import reraise +from .internal.compat import string_type +from .internal.compat import stringify +from .internal.compat import time_ns +from .internal.compat import to_unicode +from .internal.compat import urlencode + + +__all__ = [ + "CONTEXTVARS_IS_AVAILABLE", + "NumericType", + "PY2", + "PY3", + "PYTHON_INTERPRETER", + "PYTHON_VERSION", + "PYTHON_VERSION_INFO", + "Queue", + "StringIO", + "binary_type", + "contextvars", + "ensure_text", + "get_connection_response", + "getrandbits", + "httplib", + "is_integer", + "iscoroutinefunction", + "iteritems", + "main_thread", + "make_async_decorator", + "monotonic", + "monotonic_ns", + "msgpack_type", + "numeric_types", + "parse", + "pattern_type", + "process_time_ns", + "reload_module", + "reraise", + "string_type", + "stringify", + "time_ns", + "to_unicode", + "urlencode", +] + +deprecation( + name="ddtrace.compat", + message="The compat module has been moved to ddtrace.internal and will no longer be part of the public API.", + version="1.0.0", +) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/constants.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/constants.py new file mode 100644 index 000000000..0cc0f485b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/constants.py @@ -0,0 +1,38 @@ +FILTERS_KEY = "FILTERS" +SAMPLE_RATE_METRIC_KEY = "_sample_rate" +SAMPLING_PRIORITY_KEY = "_sampling_priority_v1" +ANALYTICS_SAMPLE_RATE_KEY = "_dd1.sr.eausr" +SAMPLING_AGENT_DECISION = "_dd.agent_psr" +SAMPLING_RULE_DECISION = "_dd.rule_psr" +SAMPLING_LIMIT_DECISION = "_dd.limit_psr" +ORIGIN_KEY = "_dd.origin" +HOSTNAME_KEY = "_dd.hostname" +ENV_KEY = "env" +VERSION_KEY = "version" +SERVICE_KEY = "service.name" +SERVICE_VERSION_KEY = "service.version" +SPAN_KIND = "span.kind" +SPAN_MEASURED_KEY = "_dd.measured" +KEEP_SPANS_RATE_KEY = "_dd.tracer_kr" + +NUMERIC_TAGS = (ANALYTICS_SAMPLE_RATE_KEY,) + +MANUAL_DROP_KEY = "manual.drop" +MANUAL_KEEP_KEY = "manual.keep" + +LOG_SPAN_KEY = "__datadog_log_span" + +ERROR_MSG = "error.msg" # a string representing the error message +ERROR_TYPE = "error.type" # a string representing the type of the error +ERROR_STACK = "error.stack" # a human readable version of the stack. + +PID = "system.pid" + +# Use this to explicitly inform the backend that a trace should be rejected and not stored. +USER_REJECT = -1 +# Used by the builtin sampler to inform the backend that a trace should be rejected and not stored. +AUTO_REJECT = 0 +# Used by the builtin sampler to inform the backend that a trace should be kept and stored. +AUTO_KEEP = 1 +# Use this to explicitly inform the backend that a trace should be kept and stored. +USER_KEEP = 2 diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/context.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/context.py new file mode 100644 index 000000000..b3e53d763 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/context.py @@ -0,0 +1,135 @@ +import threading +from typing import Any +from typing import Optional +from typing import TYPE_CHECKING +from typing import Text + +from .constants import ORIGIN_KEY +from .constants import SAMPLING_PRIORITY_KEY +from .internal.compat import NumericType +from .internal.logger import get_logger +from .utils.deprecation import deprecated + + +if TYPE_CHECKING: + from .span import Span + from .span import _MetaDictType + from .span import _MetricDictType + +log = get_logger(__name__) + + +class Context(object): + """Represents the state required to propagate a trace across execution + boundaries. + """ + + __slots__ = [ + "trace_id", + "span_id", + "_lock", + "_meta", + "_metrics", + ] + + def __init__( + self, + trace_id=None, # type: Optional[int] + span_id=None, # type: Optional[int] + dd_origin=None, # type: Optional[str] + sampling_priority=None, # type: Optional[float] + meta=None, # type: Optional[_MetaDictType] + metrics=None, # type: Optional[_MetricDictType] + ): + self._meta = meta if meta is not None else {} # type: _MetaDictType + self._metrics = metrics if metrics is not None else {} # type: _MetricDictType + + self.trace_id = trace_id # type: Optional[int] + self.span_id = span_id # type: Optional[int] + + if dd_origin is not None: + self._meta[ORIGIN_KEY] = dd_origin + if sampling_priority is not None: + self._metrics[SAMPLING_PRIORITY_KEY] = sampling_priority + + self._lock = threading.RLock() + + def _with_span(self, span): + # type: (Span) -> Context + """Return a shallow copy of the context with the given span.""" + ctx = self.__class__(trace_id=span.trace_id, span_id=span.span_id) + ctx._lock = self._lock + ctx._meta = self._meta + ctx._metrics = self._metrics + return ctx + + def _update_tags(self, span): + # type: (Span) -> None + with self._lock: + span.meta.update(self._meta) + span.metrics.update(self._metrics) + + @property + def sampling_priority(self): + # type: () -> Optional[NumericType] + """Return the context sampling priority for the trace.""" + return self._metrics.get(SAMPLING_PRIORITY_KEY) + + @sampling_priority.setter + def sampling_priority(self, value): + # type: (Optional[NumericType]) -> None + with self._lock: + if value is None: + if SAMPLING_PRIORITY_KEY in self._metrics: + del self._metrics[SAMPLING_PRIORITY_KEY] + return + self._metrics[SAMPLING_PRIORITY_KEY] = value + + @property + def dd_origin(self): + # type: () -> Optional[Text] + """Get the origin of the trace.""" + return self._meta.get(ORIGIN_KEY) + + @dd_origin.setter + def dd_origin(self, value): + # type: (Optional[Text]) -> None + """Set the origin of the trace.""" + with self._lock: + if value is None: + if ORIGIN_KEY in self._meta: + del self._meta[ORIGIN_KEY] + return + self._meta[ORIGIN_KEY] = value + + @deprecated("Cloning contexts will no longer be required in 0.50", version="0.50") + def clone(self): + # type: () -> Context + """ + Partially clones the current context. + It copies everything EXCEPT the registered and finished spans. + """ + return self + + def __eq__(self, other): + # type: (Any) -> bool + if isinstance(other, Context): + with self._lock: + return ( + self.trace_id == other.trace_id + and self.span_id == other.span_id + and self._meta == other._meta + and self._metrics == other._metrics + ) + return False + + def __repr__(self): + # type: () -> str + return "Context(trace_id=%s, span_id=%s, _meta=%s, _metrics=%s)" % ( + self.trace_id, + self.span_id, + self._meta, + self._metrics, + ) + + __str__ = __repr__ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/__init__.py new file mode 100644 index 000000000..bcd6ddf72 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/__init__.py @@ -0,0 +1,3 @@ +from ..utils.importlib import func_name # noqa +from ..utils.importlib import module_name # noqa +from ..utils.importlib import require_modules # noqa diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..66fad07ae Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/__pycache__/trace_utils.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/__pycache__/trace_utils.cpython-38.pyc new file mode 100644 index 000000000..3598d1d01 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/__pycache__/trace_utils.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/__pycache__/util.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/__pycache__/util.cpython-38.pyc new file mode 100644 index 000000000..260f8b220 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/__pycache__/util.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiobotocore/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiobotocore/__init__.py new file mode 100644 index 000000000..fa5a85f00 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiobotocore/__init__.py @@ -0,0 +1,30 @@ +""" +The aiobotocore integration will trace all AWS calls made with the ``aiobotocore`` +library. This integration isn't enabled when applying the default patching. +To enable it, you must run ``patch_all(aiobotocore=True)`` + +:: + + import aiobotocore.session + from ddtrace import patch + + # If not patched yet, you can patch botocore specifically + patch(aiobotocore=True) + + # This will report spans with the default instrumentation + aiobotocore.session.get_session() + lambda_client = session.create_client('lambda', region_name='us-east-1') + + # This query generates a trace + lambda_client.list_functions() +""" +from ...utils.importlib import require_modules + + +required_modules = ["aiobotocore.client"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + + __all__ = ["patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiobotocore/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiobotocore/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..f52e15ab3 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiobotocore/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiobotocore/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiobotocore/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..4dfa73953 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiobotocore/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiobotocore/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiobotocore/patch.py new file mode 100644 index 000000000..7a5dfcab9 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiobotocore/patch.py @@ -0,0 +1,136 @@ +import aiobotocore.client + +from ddtrace import config +from ddtrace.vendor import wrapt + + +try: + from aiobotocore.endpoint import ClientResponseContentProxy +except ImportError: + # aiobotocore>=0.11.0 + from aiobotocore._endpoint_helpers import ClientResponseContentProxy + +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import aws +from ...ext import http +from ...internal.compat import PYTHON_VERSION_INFO +from ...pin import Pin +from ...utils import ArgumentError +from ...utils import get_argument_value +from ...utils.formats import deep_getattr +from ...utils.wrappers import unwrap + + +ARGS_NAME = ("action", "params", "path", "verb") +TRACED_ARGS = {"params", "path", "verb"} + + +def patch(): + if getattr(aiobotocore.client, "_datadog_patch", False): + return + setattr(aiobotocore.client, "_datadog_patch", True) + + wrapt.wrap_function_wrapper("aiobotocore.client", "AioBaseClient._make_api_call", _wrapped_api_call) + Pin(service=config.service or "aws", app="aws").onto(aiobotocore.client.AioBaseClient) + + +def unpatch(): + if getattr(aiobotocore.client, "_datadog_patch", False): + setattr(aiobotocore.client, "_datadog_patch", False) + unwrap(aiobotocore.client.AioBaseClient, "_make_api_call") + + +class WrappedClientResponseContentProxy(wrapt.ObjectProxy): + def __init__(self, body, pin, parent_span): + super(WrappedClientResponseContentProxy, self).__init__(body) + self._self_pin = pin + self._self_parent_span = parent_span + + async def read(self, *args, **kwargs): + # async read that must be child of the parent span operation + operation_name = "{}.read".format(self._self_parent_span.name) + + with self._self_pin.tracer.start_span(operation_name, child_of=self._self_parent_span) as span: + # inherit parent attributes + span.resource = self._self_parent_span.resource + span.span_type = self._self_parent_span.span_type + span.meta = dict(self._self_parent_span.meta) + span.metrics = dict(self._self_parent_span.metrics) + + result = await self.__wrapped__.read(*args, **kwargs) + span.set_tag("Length", len(result)) + + return result + + # wrapt doesn't proxy `async with` context managers + if PYTHON_VERSION_INFO >= (3, 5, 0): + + async def __aenter__(self): + # call the wrapped method but return the object proxy + await self.__wrapped__.__aenter__() + return self + + async def __aexit__(self, *args, **kwargs): + response = await self.__wrapped__.__aexit__(*args, **kwargs) + return response + + +async def _wrapped_api_call(original_func, instance, args, kwargs): + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + result = await original_func(*args, **kwargs) + return result + + endpoint_name = deep_getattr(instance, "_endpoint._endpoint_prefix") + + service = pin.service if pin.service != "aws" else "{}.{}".format(pin.service, endpoint_name) + with pin.tracer.trace("{}.command".format(endpoint_name), service=service, span_type=SpanTypes.HTTP) as span: + span.set_tag(SPAN_MEASURED_KEY) + + try: + operation = get_argument_value(args, kwargs, 0, "operation_name") + span.resource = "{}.{}".format(endpoint_name, operation.lower()) + except ArgumentError: + operation = None + span.resource = endpoint_name + + aws.add_span_arg_tags(span, endpoint_name, args, ARGS_NAME, TRACED_ARGS) + + region_name = deep_getattr(instance, "meta.region_name") + + meta = { + "aws.agent": "aiobotocore", + "aws.operation": operation, + "aws.region": region_name, + } + span.set_tags(meta) + + result = await original_func(*args, **kwargs) + + body = result.get("Body") + if isinstance(body, ClientResponseContentProxy): + result["Body"] = WrappedClientResponseContentProxy(body, pin, span) + + response_meta = result["ResponseMetadata"] + response_headers = response_meta["HTTPHeaders"] + + span.set_tag(http.STATUS_CODE, response_meta["HTTPStatusCode"]) + if 500 <= response_meta["HTTPStatusCode"] < 600: + span.error = 1 + + span.set_tag("retry_attempts", response_meta["RetryAttempts"]) + + request_id = response_meta.get("RequestId") + if request_id: + span.set_tag("aws.requestid", request_id) + + request_id2 = response_headers.get("x-amz-id-2") + if request_id2: + span.set_tag("aws.requestid2", request_id2) + + # set analytics sample rate + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.aiobotocore.get_analytics_sample_rate()) + + return result diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/__init__.py new file mode 100644 index 000000000..930391e3a --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/__init__.py @@ -0,0 +1,65 @@ +""" +The ``aiohttp`` integration traces all requests defined in the application handlers. +Auto instrumentation is available using the ``trace_app`` function:: + + from aiohttp import web + from ddtrace import tracer, patch + from ddtrace.contrib.aiohttp import trace_app + + # patch third-party modules like aiohttp_jinja2 + patch(aiohttp=True) + + # create your application + app = web.Application() + app.router.add_get('/', home_handler) + + # trace your application handlers + trace_app(app, tracer, service='async-api') + web.run_app(app, port=8000) + +Integration settings are attached to your application under the ``datadog_trace`` +namespace. You can read or update them as follows:: + + # disables distributed tracing for all received requests + app['datadog_trace']['distributed_tracing_enabled'] = False + +Available settings are: + +* ``tracer`` (default: ``ddtrace.tracer``): set the default tracer instance that is used to + trace `aiohttp` internals. By default the `ddtrace` tracer is used. +* ``service`` (default: ``aiohttp-web``): set the service name used by the tracer. Usually + this configuration must be updated with a meaningful name. +* ``distributed_tracing_enabled`` (default: ``True``): enable distributed tracing during + the middleware execution, so that a new span is created with the given ``trace_id`` and + ``parent_id`` injected via request headers. + +Third-party modules that are currently supported by the ``patch()`` method are: + +* ``aiohttp_jinja2`` + +When a request span is created, a new ``Context`` for this logical execution is attached +to the ``request`` object, so that it can be used in the application code:: + + async def home_handler(request): + ctx = request['datadog_context'] + # do something with the tracing Context + +:ref:`All HTTP tags ` are supported for this integration. + +""" +from ...utils.importlib import require_modules + + +required_modules = ["aiohttp"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .middlewares import trace_app + from .patch import patch + from .patch import unpatch + + __all__ = [ + "patch", + "unpatch", + "trace_app", + ] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..0027a6fd0 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/__pycache__/middlewares.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/__pycache__/middlewares.cpython-38.pyc new file mode 100644 index 000000000..b65813a3f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/__pycache__/middlewares.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..10fc70763 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/__pycache__/template.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/__pycache__/template.cpython-38.pyc new file mode 100644 index 000000000..5be62a5c4 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/__pycache__/template.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/middlewares.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/middlewares.py new file mode 100644 index 000000000..7cb42f4e8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/middlewares.py @@ -0,0 +1,149 @@ +from ddtrace import config + +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import http +from ...internal.compat import stringify +from ..asyncio import context_provider + + +CONFIG_KEY = "datadog_trace" +REQUEST_CONTEXT_KEY = "datadog_context" +REQUEST_CONFIG_KEY = "__datadog_trace_config" +REQUEST_SPAN_KEY = "__datadog_request_span" + + +async def trace_middleware(app, handler): + """ + ``aiohttp`` middleware that traces the handler execution. + Because handlers are run in different tasks for each request, we attach the Context + instance both to the Task and to the Request objects. In this way: + + * the Task is used by the internal automatic instrumentation + * the ``Context`` attached to the request can be freely used in the application code + """ + + async def attach_context(request): + # application configs + tracer = app[CONFIG_KEY]["tracer"] + service = app[CONFIG_KEY]["service"] + distributed_tracing = app[CONFIG_KEY]["distributed_tracing_enabled"] + # Create a new context based on the propagated information. + trace_utils.activate_distributed_headers( + tracer, + int_config=config.aiohttp, + request_headers=request.headers, + override=distributed_tracing, + ) + + # trace the handler + request_span = tracer.trace( + "aiohttp.request", + service=service, + span_type=SpanTypes.WEB, + ) + request_span.set_tag(SPAN_MEASURED_KEY) + + # Configure trace search sample rate + # DEV: aiohttp is special case maintains separate configuration from config api + analytics_enabled = app[CONFIG_KEY]["analytics_enabled"] + if (config.analytics_enabled and analytics_enabled is not False) or analytics_enabled is True: + request_span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, app[CONFIG_KEY].get("analytics_sample_rate", True)) + + # attach the context and the root span to the request; the Context + # may be freely used by the application code + request[REQUEST_CONTEXT_KEY] = request_span.context + request[REQUEST_SPAN_KEY] = request_span + request[REQUEST_CONFIG_KEY] = app[CONFIG_KEY] + try: + response = await handler(request) + return response + except Exception: + request_span.set_traceback() + raise + + return attach_context + + +async def on_prepare(request, response): + """ + The on_prepare signal is used to close the request span that is created during + the trace middleware execution. + """ + # safe-guard: discard if we don't have a request span + request_span = request.get(REQUEST_SPAN_KEY, None) + if not request_span: + return + + # default resource name + resource = stringify(response.status) + + if request.match_info.route.resource: + # collect the resource name based on http resource type + res_info = request.match_info.route.resource.get_info() + + if res_info.get("path"): + resource = res_info.get("path") + elif res_info.get("formatter"): + resource = res_info.get("formatter") + elif res_info.get("prefix"): + resource = res_info.get("prefix") + + # prefix the resource name by the http method + resource = "{} {}".format(request.method, resource) + + request_span.resource = resource + + # DEV: aiohttp is special case maintains separate configuration from config api + trace_query_string = request[REQUEST_CONFIG_KEY].get("trace_query_string") + if trace_query_string is None: + trace_query_string = config.http.trace_query_string + if trace_query_string: + request_span.set_tag(http.QUERY_STRING, request.query_string) + + trace_utils.set_http_meta( + request_span, + config.aiohttp, + method=request.method, + url=str(request.url), # DEV: request.url is a yarl's URL object + status_code=response.status, + request_headers=request.headers, + response_headers=response.headers, + ) + + request_span.finish() + + +def trace_app(app, tracer, service="aiohttp-web"): + """ + Tracing function that patches the ``aiohttp`` application so that it will be + traced using the given ``tracer``. + + :param app: aiohttp application to trace + :param tracer: tracer instance to use + :param service: service name of tracer + """ + + # safe-guard: don't trace an application twice + if getattr(app, "__datadog_trace", False): + return + setattr(app, "__datadog_trace", True) + + # configure datadog settings + app[CONFIG_KEY] = { + "tracer": tracer, + "service": config._get_service(default=service), + "distributed_tracing_enabled": None, + "analytics_enabled": None, + "analytics_sample_rate": 1.0, + } + + # the tracer must work with asynchronous Context propagation + tracer.configure(context_provider=context_provider) + + # add the async tracer middleware as a first middleware + # and be sure that the on_prepare signal is the last one + app.middlewares.insert(0, trace_middleware) + app.on_response_prepare.append(on_prepare) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/patch.py new file mode 100644 index 000000000..e7c12c7e1 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/patch.py @@ -0,0 +1,54 @@ +from ddtrace import config +from ddtrace.internal.logger import get_logger +from ddtrace.vendor import wrapt + +from ...pin import Pin +from ...utils.wrappers import unwrap + + +log = get_logger(__name__) + +try: + # instrument external packages only if they're available + import aiohttp_jinja2 + + from .template import _trace_render_template + + template_module = True +except ImportError: + template_module = False +except Exception: + log.warning("aiohttp_jinja2 could not be imported and will not be instrumented.", exc_info=True) + template_module = False + +config._add( + "aiohttp", + dict( + distributed_tracing=True, + ), +) + + +def patch(): + """ + Patch aiohttp third party modules: + * aiohttp_jinja2 + """ + if template_module: + if getattr(aiohttp_jinja2, "__datadog_patch", False): + return + setattr(aiohttp_jinja2, "__datadog_patch", True) + + _w = wrapt.wrap_function_wrapper + _w("aiohttp_jinja2", "render_template", _trace_render_template) + Pin(app="aiohttp", service=config.service).onto(aiohttp_jinja2) + + +def unpatch(): + """ + Remove tracing from patched modules. + """ + if template_module: + if getattr(aiohttp_jinja2, "__datadog_patch", False): + setattr(aiohttp_jinja2, "__datadog_patch", False) + unwrap(aiohttp_jinja2, "render_template") diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/template.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/template.py new file mode 100644 index 000000000..fdbdc2816 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiohttp/template.py @@ -0,0 +1,30 @@ +import aiohttp_jinja2 + +from ddtrace import Pin + +from ...ext import SpanTypes +from ...utils import get_argument_value + + +def _trace_render_template(func, module, args, kwargs): + """ + Trace the template rendering + """ + # get the module pin + pin = Pin.get_from(aiohttp_jinja2) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + # original signature: + # render_template(template_name, request, context, *, app_key=APP_KEY, encoding='utf-8') + template_name = get_argument_value(args, kwargs, 0, "template_name") + request = get_argument_value(args, kwargs, 1, "request") + env = aiohttp_jinja2.get_env(request.app) + + # the prefix is available only on PackageLoader + template_prefix = getattr(env.loader, "package_path", "") + template_meta = "{}/{}".format(template_prefix, template_name) + + with pin.tracer.trace("aiohttp.template", span_type=SpanTypes.TEMPLATE) as span: + span.set_meta("aiohttp.template", template_meta) + return func(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/__init__.py new file mode 100644 index 000000000..f7458296b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/__init__.py @@ -0,0 +1,27 @@ +""" +Instrument aiopg to report a span for each executed Postgres queries:: + + from ddtrace import Pin, patch + import aiopg + + # If not patched yet, you can patch aiopg specifically + patch(aiopg=True) + + # This will report a span with the default settings + async with aiopg.connect(DSN) as db: + with (await db.cursor()) as cursor: + await cursor.execute("SELECT * FROM users WHERE id = 1") + + # Use a pin to specify metadata related to this connection + Pin.override(db, service='postgres-users') +""" +from ...utils.importlib import require_modules + + +required_modules = ["aiopg"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + + __all__ = ["patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..0db0c6c3d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/__pycache__/connection.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/__pycache__/connection.cpython-38.pyc new file mode 100644 index 000000000..3b0a26615 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/__pycache__/connection.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..13aa75d1f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/connection.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/connection.py new file mode 100644 index 000000000..4319e53f3 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/connection.py @@ -0,0 +1,111 @@ +import asyncio + +from aiopg import __version__ +from aiopg.utils import _ContextManager + +from ddtrace import config +from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY +from ddtrace.constants import SPAN_MEASURED_KEY +from ddtrace.contrib import dbapi +from ddtrace.ext import SpanTypes +from ddtrace.ext import sql +from ddtrace.pin import Pin +from ddtrace.utils.version import parse_version +from ddtrace.vendor import wrapt + + +AIOPG_VERSION = parse_version(__version__) + + +class AIOTracedCursor(wrapt.ObjectProxy): + """TracedCursor wraps a psql cursor and traces its queries.""" + + def __init__(self, cursor, pin): + super(AIOTracedCursor, self).__init__(cursor) + pin.onto(self) + name = pin.app or "sql" + self._datadog_name = "%s.query" % name + + @asyncio.coroutine + def _trace_method(self, method, resource, extra_tags, *args, **kwargs): + pin = Pin.get_from(self) + if not pin or not pin.enabled(): + result = yield from method(*args, **kwargs) + return result + service = pin.service + + with pin.tracer.trace(self._datadog_name, service=service, resource=resource, span_type=SpanTypes.SQL) as s: + s.set_tag(SPAN_MEASURED_KEY) + s.set_tag(sql.QUERY, resource) + s.set_tags(pin.tags) + s.set_tags(extra_tags) + + # set analytics sample rate + s.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.aiopg.get_analytics_sample_rate()) + + try: + result = yield from method(*args, **kwargs) + return result + finally: + s.set_metric("db.rowcount", self.rowcount) + + @asyncio.coroutine + def executemany(self, query, *args, **kwargs): + # FIXME[matt] properly handle kwargs here. arg names can be different + # with different libs. + result = yield from self._trace_method( + self.__wrapped__.executemany, query, {"sql.executemany": "true"}, query, *args, **kwargs + ) + return result + + @asyncio.coroutine + def execute(self, query, *args, **kwargs): + result = yield from self._trace_method(self.__wrapped__.execute, query, {}, query, *args, **kwargs) + return result + + @asyncio.coroutine + def callproc(self, proc, args): + result = yield from self._trace_method(self.__wrapped__.callproc, proc, {}, proc, args) + return result + + def __aiter__(self): + return self.__wrapped__.__aiter__() + + +class AIOTracedConnection(wrapt.ObjectProxy): + """TracedConnection wraps a Connection with tracing code.""" + + def __init__(self, conn, pin=None, cursor_cls=AIOTracedCursor): + super(AIOTracedConnection, self).__init__(conn) + name = dbapi._get_vendor(conn) + db_pin = pin or Pin(service=name, app=name) + db_pin.onto(self) + # wrapt requires prefix of `_self` for attributes that are only in the + # proxy (since some of our source objects will use `__slots__`) + self._self_cursor_cls = cursor_cls + + # unfortunately we also need to patch this method as otherwise "self" + # ends up being the aiopg connection object + if AIOPG_VERSION >= (0, 16, 0): + + def cursor(self, *args, **kwargs): + # Only one cursor per connection is allowed, as per DB API spec + self.close_cursor() + self._last_usage = self._loop.time() + + coro = self._cursor(*args, **kwargs) + return _ContextManager(coro) + + else: + + def cursor(self, *args, **kwargs): + coro = self._cursor(*args, **kwargs) + return _ContextManager(coro) + + @asyncio.coroutine + def _cursor(self, *args, **kwargs): + cursor = yield from self.__wrapped__._cursor(*args, **kwargs) + pin = Pin.get_from(self) + if not pin: + return cursor + return self._self_cursor_cls(cursor, pin) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/patch.py new file mode 100644 index 000000000..8a2fc3d37 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aiopg/patch.py @@ -0,0 +1,57 @@ +# 3p +import asyncio + +import aiopg.connection +import psycopg2.extensions + +from ddtrace.contrib.aiopg.connection import AIOTracedConnection +from ddtrace.contrib.psycopg.patch import _patch_extensions +from ddtrace.contrib.psycopg.patch import _unpatch_extensions +from ddtrace.contrib.psycopg.patch import patch_conn as psycopg_patch_conn +from ddtrace.utils.wrappers import unwrap as _u +from ddtrace.vendor import wrapt + + +def patch(): + """Patch monkey patches psycopg's connection function + so that the connection's functions are traced. + """ + if getattr(aiopg, "_datadog_patch", False): + return + setattr(aiopg, "_datadog_patch", True) + + wrapt.wrap_function_wrapper(aiopg.connection, "_connect", patched_connect) + _patch_extensions(_aiopg_extensions) # do this early just in case + + +def unpatch(): + if getattr(aiopg, "_datadog_patch", False): + setattr(aiopg, "_datadog_patch", False) + _u(aiopg.connection, "_connect") + _unpatch_extensions(_aiopg_extensions) + + +@asyncio.coroutine +def patched_connect(connect_func, _, args, kwargs): + conn = yield from connect_func(*args, **kwargs) + return psycopg_patch_conn(conn, traced_conn_cls=AIOTracedConnection) + + +def _extensions_register_type(func, _, args, kwargs): + def _unroll_args(obj, scope=None): + return obj, scope + + obj, scope = _unroll_args(*args, **kwargs) + + # register_type performs a c-level check of the object + # type so we must be sure to pass in the actual db connection + if scope and isinstance(scope, wrapt.ObjectProxy): + scope = scope.__wrapped__._conn + + return func(obj, scope) if scope else func(obj) + + +# extension hooks +_aiopg_extensions = [ + (psycopg2.extensions.register_type, psycopg2.extensions, "register_type", _extensions_register_type), +] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/algoliasearch/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/algoliasearch/__init__.py new file mode 100644 index 000000000..41bb48da8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/algoliasearch/__init__.py @@ -0,0 +1,34 @@ +""" +The Algoliasearch__ integration will add tracing to your Algolia searches. + +:: + + from ddtrace import patch_all + patch_all() + + from algoliasearch import algoliasearch + client = alogliasearch.Client(, ) + index = client.init_index() + index.search("your query", args={"attributesToRetrieve": "attribute1,attribute1"}) + +Configuration +~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.algoliasearch['collect_query_text'] + + Whether to pass the text of your query onto Datadog. Since this may contain sensitive data it's off by default + + Default: ``False`` + +.. __: https://www.algolia.com +""" + +from ...utils.importlib import require_modules + + +with require_modules(["algoliasearch", "algoliasearch.version"]) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = ["patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/algoliasearch/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/algoliasearch/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..f8bda3089 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/algoliasearch/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/algoliasearch/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/algoliasearch/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..868b29ccf Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/algoliasearch/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/algoliasearch/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/algoliasearch/patch.py new file mode 100644 index 000000000..6bb88e209 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/algoliasearch/patch.py @@ -0,0 +1,152 @@ +from ddtrace import config +from ddtrace.ext import SpanTypes +from ddtrace.pin import Pin +from ddtrace.utils.wrappers import unwrap as _u +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from .. import trace_utils +from ...constants import SPAN_MEASURED_KEY + + +DD_PATCH_ATTR = "_datadog_patch" + +SERVICE_NAME = "algoliasearch" +APP_NAME = "algoliasearch" + +try: + import algoliasearch + from algoliasearch.version import VERSION + + algoliasearch_version = tuple([int(i) for i in VERSION.split(".")]) + + # Default configuration + config._add("algoliasearch", dict(_default_service=SERVICE_NAME, collect_query_text=False)) +except ImportError: + algoliasearch_version = (0, 0) + + +def patch(): + if algoliasearch_version == (0, 0): + return + + if getattr(algoliasearch, DD_PATCH_ATTR, False): + return + + setattr(algoliasearch, "_datadog_patch", True) + + pin = Pin(app=APP_NAME) + + if algoliasearch_version < (2, 0) and algoliasearch_version >= (1, 0): + _w(algoliasearch.index, "Index.search", _patched_search) + pin.onto(algoliasearch.index.Index) + elif algoliasearch_version >= (2, 0) and algoliasearch_version < (3, 0): + from algoliasearch import search_index + + _w(algoliasearch, "search_index.SearchIndex.search", _patched_search) + pin.onto(search_index.SearchIndex) + else: + return + + +def unpatch(): + if algoliasearch_version == (0, 0): + return + + if getattr(algoliasearch, DD_PATCH_ATTR, False): + setattr(algoliasearch, DD_PATCH_ATTR, False) + + if algoliasearch_version < (2, 0) and algoliasearch_version >= (1, 0): + _u(algoliasearch.index.Index, "search") + elif algoliasearch_version >= (2, 0) and algoliasearch_version < (3, 0): + from algoliasearch import search_index + + _u(search_index.SearchIndex, "search") + else: + return + + +# DEV: this maps serves the dual purpose of enumerating the algoliasearch.search() query_args that +# will be sent along as tags, as well as converting arguments names into tag names compliant with +# tag naming recommendations set out here: https://docs.datadoghq.com/tagging/ +QUERY_ARGS_DD_TAG_MAP = { + "page": "page", + "hitsPerPage": "hits_per_page", + "attributesToRetrieve": "attributes_to_retrieve", + "attributesToHighlight": "attributes_to_highlight", + "attributesToSnippet": "attributes_to_snippet", + "minWordSizefor1Typo": "min_word_size_for_1_typo", + "minWordSizefor2Typos": "min_word_size_for_2_typos", + "getRankingInfo": "get_ranking_info", + "aroundLatLng": "around_lat_lng", + "numericFilters": "numeric_filters", + "tagFilters": "tag_filters", + "queryType": "query_type", + "optionalWords": "optional_words", + "distinct": "distinct", +} + + +def _patched_search(func, instance, wrapt_args, wrapt_kwargs): + """ + wrapt_args is called the way it is to distinguish it from the 'args' + argument to the algoliasearch.index.Index.search() method. + """ + + if algoliasearch_version < (2, 0) and algoliasearch_version >= (1, 0): + function_query_arg_name = "args" + elif algoliasearch_version >= (2, 0) and algoliasearch_version < (3, 0): + function_query_arg_name = "request_options" + else: + return func(*wrapt_args, **wrapt_kwargs) + + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return func(*wrapt_args, **wrapt_kwargs) + + with pin.tracer.trace( + "algoliasearch.search", + service=trace_utils.ext_service(pin, config.algoliasearch), + span_type=SpanTypes.HTTP, + ) as span: + span.set_tag(SPAN_MEASURED_KEY) + + if not span.sampled: + return func(*wrapt_args, **wrapt_kwargs) + + if config.algoliasearch.collect_query_text: + span.set_tag("query.text", wrapt_kwargs.get("query", wrapt_args[0])) + + query_args = wrapt_kwargs.get(function_query_arg_name, wrapt_args[1] if len(wrapt_args) > 1 else None) + + if query_args and isinstance(query_args, dict): + for query_arg, tag_name in QUERY_ARGS_DD_TAG_MAP.items(): + value = query_args.get(query_arg) + if value is not None: + span.set_tag("query.args.{}".format(tag_name), value) + + # Result would look like this + # { + # 'hits': [ + # { + # .... your search results ... + # } + # ], + # 'processingTimeMS': 1, + # 'nbHits': 1, + # 'hitsPerPage': 20, + # 'exhaustiveNbHits': true, + # 'params': 'query=xxx', + # 'nbPages': 1, + # 'query': 'xxx', + # 'page': 0 + # } + result = func(*wrapt_args, **wrapt_kwargs) + + if isinstance(result, dict): + if result.get("processingTimeMS", None) is not None: + span.set_metric("processing_time_ms", int(result["processingTimeMS"])) + + if result.get("nbHits", None) is not None: + span.set_metric("number_of_hits", int(result["nbHits"])) + + return result diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aredis/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aredis/__init__.py new file mode 100644 index 000000000..9d765dd68 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aredis/__init__.py @@ -0,0 +1,58 @@ +""" +The aredis integration traces aredis requests. + + +Enabling +~~~~~~~~ + +The aredis integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(aredis=True) + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.aredis["service"] + + The service name reported by default for aredis traces. + + This option can also be set with the ``DD_AREDIS_SERVICE`` environment + variable. + + Default: ``"redis"`` + + +Instance Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +To configure particular aredis instances use the :ref:`Pin` API:: + + import aredis + from ddtrace import Pin + + client = aredis.StrictRedis(host="localhost", port=6379) + + # Override service name for this instance + Pin.override(client, service="my-custom-queue") + + # Traces reported for this client will now have "my-custom-queue" + # as the service name. + async def example(): + await client.get("my-key") +""" + +from ...utils.importlib import require_modules + + +required_modules = ["aredis", "aredis.client"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + + __all__ = ["patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aredis/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aredis/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..1bc37d65c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aredis/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aredis/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aredis/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..0675ccf47 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aredis/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aredis/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aredis/patch.py new file mode 100644 index 000000000..f9314641c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/aredis/patch.py @@ -0,0 +1,153 @@ +import aredis + +from ddtrace import config +from ddtrace.vendor import wrapt + +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import net +from ...ext import redis as redisx +from ...internal.compat import stringify +from ...pin import Pin +from ...utils.wrappers import unwrap + + +VALUE_PLACEHOLDER = "?" +VALUE_MAX_LEN = 100 +VALUE_TOO_LONG_MARK = "..." +CMD_MAX_LEN = 1000 + + +config._add("aredis", dict(_default_service="redis")) + + +def patch(): + """Patch the instrumented methods""" + if getattr(aredis, "_datadog_patch", False): + return + setattr(aredis, "_datadog_patch", True) + + _w = wrapt.wrap_function_wrapper + + _w("aredis.client", "StrictRedis.execute_command", traced_execute_command) + _w("aredis.client", "StrictRedis.pipeline", traced_pipeline) + _w("aredis.pipeline", "StrictPipeline.execute", traced_execute_pipeline) + _w("aredis.pipeline", "StrictPipeline.immediate_execute_command", traced_execute_command) + Pin(service=None, app=redisx.APP).onto(aredis.StrictRedis) + + +def unpatch(): + if getattr(aredis, "_datadog_patch", False): + setattr(aredis, "_datadog_patch", False) + + unwrap(aredis.client.StrictRedis, "execute_command") + unwrap(aredis.client.StrictRedis, "pipeline") + unwrap(aredis.pipeline.StrictPipeline, "execute") + unwrap(aredis.pipeline.StrictPipeline, "immediate_execute_command") + + +# +# tracing functions +# +async def traced_execute_command(func, instance, args, kwargs): + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return await func(*args, **kwargs) + + with pin.tracer.trace( + redisx.CMD, service=trace_utils.ext_service(pin, config.aredis, pin), span_type=SpanTypes.REDIS + ) as s: + s.set_tag(SPAN_MEASURED_KEY) + query = format_command_args(args) + s.resource = query + s.set_tag(redisx.RAWCMD, query) + if pin.tags: + s.set_tags(pin.tags) + s.set_tags(_get_tags(instance)) + s.set_metric(redisx.ARGS_LEN, len(args)) + # set analytics sample rate if enabled + s.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.aredis.get_analytics_sample_rate()) + # run the command + return await func(*args, **kwargs) + + +async def traced_pipeline(func, instance, args, kwargs): + pipeline = await func(*args, **kwargs) + pin = Pin.get_from(instance) + if pin: + pin.onto(pipeline) + return pipeline + + +async def traced_execute_pipeline(func, instance, args, kwargs): + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return await func(*args, **kwargs) + + # FIXME[matt] done in the agent. worth it? + cmds = [format_command_args(c) for c, _ in instance.command_stack] + resource = "\n".join(cmds) + tracer = pin.tracer + with tracer.trace( + redisx.CMD, + resource=resource, + service=trace_utils.ext_service(pin, config.aredis), + span_type=SpanTypes.REDIS, + ) as s: + s.set_tag(SPAN_MEASURED_KEY) + s.set_tag(redisx.RAWCMD, resource) + s.set_tags(_get_tags(instance)) + s.set_metric(redisx.PIPELINE_LEN, len(instance.command_stack)) + + # set analytics sample rate if enabled + s.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.aredis.get_analytics_sample_rate()) + + return await func(*args, **kwargs) + + +def _extract_conn_tags(conn_kwargs): + """Transform aredis conn info into dogtrace metas""" + try: + return { + net.TARGET_HOST: conn_kwargs["host"], + net.TARGET_PORT: conn_kwargs["port"], + redisx.DB: conn_kwargs["db"] or 0, + } + except Exception: + return {} + + +def format_command_args(args): + """Format a command by removing unwanted values + + Restrict what we keep from the values sent (with a SET, HGET, LPUSH, ...): + - Skip binary content + - Truncate + """ + length = 0 + out = [] + for arg in args: + try: + cmd = stringify(arg) + + if len(cmd) > VALUE_MAX_LEN: + cmd = cmd[:VALUE_MAX_LEN] + VALUE_TOO_LONG_MARK + + if length + len(cmd) > CMD_MAX_LEN: + prefix = cmd[: CMD_MAX_LEN - length] + out.append("%s%s" % (prefix, VALUE_TOO_LONG_MARK)) + break + + out.append(cmd) + length += len(cmd) + except Exception: + out.append(VALUE_PLACEHOLDER) + break + + return " ".join(out) + + +def _get_tags(conn): + return _extract_conn_tags(conn.connection_pool.connection_kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/__init__.py new file mode 100644 index 000000000..b3d9981b8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/__init__.py @@ -0,0 +1,75 @@ +""" +The asgi__ middleware for tracing all requests to an ASGI-compliant application. + +To configure tracing manually:: + + from ddtrace.contrib.asgi import TraceMiddleware + + # app = + app = TraceMiddleware(app) + +Then use ddtrace-run when serving your application. For example, if serving with Uvicorn:: + + ddtrace-run uvicorn app:app + +If using Python 3.6, the legacy ``AsyncioContextProvider`` will have to be +enabled before using the middleware:: + + from ddtrace.contrib.asyncio.provider import AsyncioContextProvider + from ddtrace import tracer # Or whichever tracer instance you plan to use + tracer.configure(context_provider=AsyncioContextProvider()) + +The middleware also supports using a custom function for handling exceptions for a trace:: + + from ddtrace.contrib.asgi import TraceMiddleware + + def custom_handle_exception_span(exc, span): + span.set_tag("http.status_code", 501) + + # app = + app = TraceMiddleware(app, handle_exception_span=custom_handle_exception_span) + + +To retrieve the request span from the scope of an ASGI request use the ``span_from_scope`` +function:: + + from ddtrace.contrib.asgi import span_from_scope + + def handle_request(scope, send): + span = span_from_scope(scope) + if span: + span.set_tag(...) + ... + + +Configuration +~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.asgi['distributed_tracing'] + + Whether to use distributed tracing headers from requests received by your Asgi app. + + Default: ``True`` + +.. py:data:: ddtrace.config.asgi['service_name'] + + The service name reported for your ASGI app. + + Can also be configured via the ``DD_SERVICE`` environment variable. + + Default: ``'asgi'`` + +.. __: https://asgi.readthedocs.io/ +""" + +from ...utils.importlib import require_modules + + +required_modules = [] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .middleware import TraceMiddleware + from .middleware import span_from_scope + + __all__ = ["TraceMiddleware", "span_from_scope"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..50a4d485f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/__pycache__/middleware.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/__pycache__/middleware.cpython-38.pyc new file mode 100644 index 000000000..888995b14 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/__pycache__/middleware.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/__pycache__/utils.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/__pycache__/utils.cpython-38.pyc new file mode 100644 index 000000000..bf24d0900 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/__pycache__/utils.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/middleware.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/middleware.py new file mode 100644 index 000000000..d86aa8b81 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/middleware.py @@ -0,0 +1,184 @@ +import sys +from typing import TYPE_CHECKING + +import ddtrace +from ddtrace import config +from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY +from ddtrace.ext import SpanTypes +from ddtrace.ext import http + +from .. import trace_utils +from ...internal.compat import reraise +from ...internal.logger import get_logger +from .utils import guarantee_single_callable + + +if TYPE_CHECKING: + from typing import Any + from typing import Mapping + from typing import Optional + + from ddtrace import Span + + +log = get_logger(__name__) + +config._add( + "asgi", + dict(service_name=config._get_service(default="asgi"), request_span_name="asgi.request", distributed_tracing=True), +) + +ASGI_VERSION = "asgi.version" +ASGI_SPEC_VERSION = "asgi.spec_version" + + +def bytes_to_str(str_or_bytes): + return str_or_bytes.decode() if isinstance(str_or_bytes, bytes) else str_or_bytes + + +def _extract_versions_from_scope(scope, integration_config): + tags = {} + + http_version = scope.get("http_version") + if http_version: + tags[http.VERSION] = http_version + + scope_asgi = scope.get("asgi") + + if scope_asgi and "version" in scope_asgi: + tags[ASGI_VERSION] = scope_asgi["version"] + + if scope_asgi and "spec_version" in scope_asgi: + tags[ASGI_SPEC_VERSION] = scope_asgi["spec_version"] + + return tags + + +def _extract_headers(scope): + headers = scope.get("headers") + if headers: + # headers: (Iterable[[byte string, byte string]]) + return dict((bytes_to_str(k), bytes_to_str(v)) for (k, v) in headers) + return {} + + +def _default_handle_exception_span(exc, span): + """Default handler for exception for span""" + span.set_tag(http.STATUS_CODE, 500) + + +def span_from_scope(scope): + # type: (Mapping[str, Any]) -> Optional[Span] + """Retrieve the top-level ASGI span from the scope.""" + return scope.get("datadog", {}).get("request_span") + + +class TraceMiddleware: + """ + ASGI application middleware that traces the requests. + + Args: + app: The ASGI application. + tracer: Custom tracer. Defaults to the global tracer. + """ + + def __init__( + self, + app, + tracer=None, + integration_config=config.asgi, + handle_exception_span=_default_handle_exception_span, + span_modifier=None, + ): + self.app = guarantee_single_callable(app) + self.tracer = tracer or ddtrace.tracer + self.integration_config = integration_config + self.handle_exception_span = handle_exception_span + self.span_modifier = span_modifier + + async def __call__(self, scope, receive, send): + if scope["type"] != "http": + return await self.app(scope, receive, send) + + try: + headers = _extract_headers(scope) + except Exception: + log.warning("failed to decode headers for distributed tracing", exc_info=True) + headers = {} + else: + trace_utils.activate_distributed_headers( + self.tracer, int_config=self.integration_config, request_headers=headers + ) + + resource = "{} {}".format(scope["method"], scope["path"]) + + span = self.tracer.trace( + name=self.integration_config.get("request_span_name", "asgi.request"), + service=trace_utils.int_service(None, self.integration_config), + resource=resource, + span_type=SpanTypes.WEB, + ) + + scope["datadog"] = {"request_span": span} + + if self.span_modifier: + self.span_modifier(span, scope) + + sample_rate = self.integration_config.get_analytics_sample_rate(use_global_config=True) + if sample_rate is not None: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate) + + method = scope.get("method") + server = scope.get("server") + if server and len(server) == 2: + port = server[1] + server_host = server[0] + (":" + str(port) if port is not None and port != 80 else "") + full_path = scope.get("root_path", "") + scope.get("path", "") + url = scope.get("scheme", "http") + "://" + server_host + full_path + else: + url = None + + if self.integration_config.trace_query_string: + query_string = scope.get("query_string") + if len(query_string) > 0: + query_string = bytes_to_str(query_string) + else: + query_string = None + + trace_utils.set_http_meta( + span, self.integration_config, method=method, url=url, query=query_string, request_headers=headers + ) + + tags = _extract_versions_from_scope(scope, self.integration_config) + span.set_tags(tags) + + async def wrapped_send(message): + if span and message.get("type") == "http.response.start" and "status" in message: + status_code = message["status"] + else: + status_code = None + + if "headers" in message: + response_headers = message["headers"] + else: + response_headers = None + + trace_utils.set_http_meta( + span, self.integration_config, status_code=status_code, response_headers=response_headers + ) + + return await send(message) + + try: + return await self.app(scope, receive, wrapped_send) + except Exception as exc: + (exc_type, exc_val, exc_tb) = sys.exc_info() + span.set_exc_info(exc_type, exc_val, exc_tb) + self.handle_exception_span(exc, span) + reraise(exc_type, exc_val, exc_tb) + finally: + try: + del scope["datadog"]["request_span"] + except KeyError: + pass + span.finish() diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/utils.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/utils.py new file mode 100644 index 000000000..0551352dc --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asgi/utils.py @@ -0,0 +1,82 @@ +""" +Compatibility functions vendored from asgiref + +Source: https://github.com/django/asgiref +Version: 3.2.10 +License: + +Copyright (c) Django Software Foundation and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of Django nor the names of its contributors may be used + to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" +import asyncio +import inspect + + +def is_double_callable(application): + """ + Tests to see if an application is a legacy-style (double-callable) application. + """ + # Look for a hint on the object first + if getattr(application, "_asgi_single_callable", False): + return False + if getattr(application, "_asgi_double_callable", False): + return True + # Uninstanted classes are double-callable + if inspect.isclass(application): + return True + # Instanted classes depend on their __call__ + if hasattr(application, "__call__"): + # We only check to see if its __call__ is a coroutine function - + # if it's not, it still might be a coroutine function itself. + if asyncio.iscoroutinefunction(application.__call__): + return False + # Non-classes we just check directly + return not asyncio.iscoroutinefunction(application) + + +def double_to_single_callable(application): + """ + Transforms a double-callable ASGI application into a single-callable one. + """ + + async def new_application(scope, receive, send): + instance = application(scope) + return await instance(receive, send) + + return new_application + + +def guarantee_single_callable(application): + """ + Takes either a single- or double-callable application and always returns it + in single-callable style. Use this to add backwards compatibility for ASGI + 2.0 applications to your server/test harness/etc. + """ + if is_double_callable(application): + application = double_to_single_callable(application) + return application diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__init__.py new file mode 100644 index 000000000..b1cb683d1 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__init__.py @@ -0,0 +1,62 @@ +""" +This integration provides automatic instrumentation to trace the execution flow +of concurrent execution of ``asyncio.Task``. Also it provides a legacy context +provider for supporting tracing of asynchronous execution in Python < 3.7. + +For asynchronous execution tracing in Python < 3.7 to work properly the tracer must +be configured as follows:: + + import asyncio + from ddtrace import tracer + from ddtrace.contrib.asyncio import context_provider + + # enable asyncio support + tracer.configure(context_provider=context_provider) + + async def some_work(): + with tracer.trace('asyncio.some_work'): + # do something + + # launch your coroutines as usual + loop = asyncio.get_event_loop() + loop.run_until_complete(some_work()) + loop.close() + +In addition, helpers are provided to simplify how the tracing ``Context`` is +handled between scheduled coroutines and ``Future`` invoked in separated +threads: + + * ``set_call_context(task, ctx)``: attach the context to the given ``Task`` + so that it will be available from the ``tracer.get_call_context()`` + * ``ensure_future(coro_or_future, *, loop=None)``: wrapper for the + ``asyncio.ensure_future`` that attaches the current context to a new + ``Task`` instance + * ``run_in_executor(loop, executor, func, *args)``: wrapper for the + ``loop.run_in_executor`` that attaches the current context to the new + thread so that the trace can be resumed regardless when it's executed + * ``create_task(coro)``: creates a new asyncio ``Task`` that inherits the + current active ``Context`` so that generated traces in the new task are + attached to the main trace +""" +from ...utils.importlib import require_modules + + +required_modules = ["asyncio"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from ...internal.compat import CONTEXTVARS_IS_AVAILABLE + from ...provider import DefaultContextProvider + from .provider import AsyncioContextProvider + + if CONTEXTVARS_IS_AVAILABLE: + context_provider = DefaultContextProvider() + else: + context_provider = AsyncioContextProvider() + + from .helpers import ensure_future + from .helpers import run_in_executor + from .helpers import set_call_context + from .patch import patch + + __all__ = ["context_provider", "set_call_context", "ensure_future", "run_in_executor", "patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..ed79243e9 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/compat.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/compat.cpython-38.pyc new file mode 100644 index 000000000..e73df6c4e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/compat.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/helpers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/helpers.cpython-38.pyc new file mode 100644 index 000000000..36b8c017f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/helpers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..6a8556296 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/provider.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/provider.cpython-38.pyc new file mode 100644 index 000000000..4cfc26c36 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/provider.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/wrappers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/wrappers.cpython-38.pyc new file mode 100644 index 000000000..a5a75d435 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/__pycache__/wrappers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/compat.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/compat.py new file mode 100644 index 000000000..104154e37 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/compat.py @@ -0,0 +1,16 @@ +import asyncio + + +if hasattr(asyncio, "current_task"): + + def asyncio_current_task(): + try: + return asyncio.current_task() + except RuntimeError: + return None + + +else: + + def asyncio_current_task(): + return asyncio.Task.current_task() diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/helpers.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/helpers.py new file mode 100644 index 000000000..f1803ee40 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/helpers.py @@ -0,0 +1,81 @@ +""" +This module includes a list of convenience methods that +can be used to simplify some operations while handling +Context and Spans in instrumented ``asyncio`` code. +""" +import asyncio + +import ddtrace + +from .provider import AsyncioContextProvider +from .wrappers import wrapped_create_task + + +def set_call_context(task, ctx): + """ + Updates the ``Context`` for the given Task. Useful when you need to + pass the context among different tasks. + + This method is available for backward-compatibility. Use the + ``AsyncioContextProvider`` API to set the current active ``Context``. + """ + setattr(task, AsyncioContextProvider._CONTEXT_ATTR, ctx) + + +def ensure_future(coro_or_future, *, loop=None, tracer=None): + """Wrapper that sets a context to the newly created Task. + + If the current task already has a Context, it will be attached to the new Task so the Trace list will be preserved. + """ + tracer = tracer or ddtrace.tracer + current_ctx = tracer.current_trace_context() + task = asyncio.ensure_future(coro_or_future, loop=loop) + set_call_context(task, current_ctx) + return task + + +def run_in_executor(loop, executor, func, *args, tracer=None): + """Wrapper function that sets a context to the newly created Thread. + + If the current task has a Context, it will be attached as an empty Context with the current_span activated to + inherit the ``trace_id`` and the ``parent_id``. + + Because the Executor can run the Thread immediately or after the + coroutine is executed, we may have two different scenarios: + * the Context is copied in the new Thread and the trace is sent twice + * the coroutine flushes the Context and when the Thread copies the + Context it is already empty (so it will be a root Span) + + To support both situations, we create a new Context that knows only what was + the latest active Span when the new thread was created. In this new thread, + we fallback to the thread-local ``Context`` storage. + + """ + tracer = tracer or ddtrace.tracer + current_ctx = tracer.current_trace_context() + + # prepare the future using an executor wrapper + future = loop.run_in_executor(executor, _wrap_executor, func, args, tracer, current_ctx) + return future + + +def _wrap_executor(fn, args, tracer, ctx): + """ + This function is executed in the newly created Thread so the right + ``Context`` can be set in the thread-local storage. This operation + is safe because the ``Context`` class is thread-safe and can be + updated concurrently. + """ + # the AsyncioContextProvider knows that this is a new thread + # so it is legit to pass the Context in the thread-local storage; + # fn() will be executed outside the asyncio loop as a synchronous code + tracer.context_provider.activate(ctx) + return fn(*args) + + +def create_task(*args, **kwargs): + """This function spawns a task with a Context that inherits the + `trace_id` and the `parent_id` from the current active one if available. + """ + loop = asyncio.get_event_loop() + return wrapped_create_task(loop.create_task, None, args, kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/patch.py new file mode 100644 index 000000000..257576709 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/patch.py @@ -0,0 +1,40 @@ +import asyncio +import sys + +from ddtrace.vendor.wrapt import ObjectProxy +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from ...utils.wrappers import unwrap as _u +from .wrappers import wrapped_create_task + + +def patch(): + """Patches current loop `create_task()` method to enable spawned tasks to + parent to the base task context. + """ + if getattr(asyncio, "_datadog_patch", False): + return + setattr(asyncio, "_datadog_patch", True) + + if sys.version_info < (3, 7, 0): + _w(asyncio.BaseEventLoop, "create_task", wrapped_create_task) + + # also patch event loop if not inheriting the wrapped create_task from BaseEventLoop + loop = asyncio.get_event_loop() + if not isinstance(loop.create_task, ObjectProxy): + _w(loop, "create_task", wrapped_create_task) + + +def unpatch(): + """Remove tracing from patched modules.""" + + if getattr(asyncio, "_datadog_patch", False): + setattr(asyncio, "_datadog_patch", False) + + if sys.version_info < (3, 7, 0): + _u(asyncio.BaseEventLoop, "create_task") + + # also unpatch event loop if not inheriting the already unwrapped create_task from BaseEventLoop + loop = asyncio.get_event_loop() + if isinstance(loop.create_task, ObjectProxy): + _u(loop, "create_task") diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/provider.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/provider.py new file mode 100644 index 000000000..356753c1e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/provider.py @@ -0,0 +1,74 @@ +import asyncio + +from ...provider import BaseContextProvider +from ...provider import DatadogContextMixin +from ...span import Span + + +class AsyncioContextProvider(BaseContextProvider, DatadogContextMixin): + """Manages the active context for asyncio execution. Framework + instrumentation that is built on top of the ``asyncio`` library, should + use this provider when contextvars are not available (Python versions + less than 3.7). + + This Context Provider inherits from ``DefaultContextProvider`` because + it uses a thread-local storage when the ``Context`` is propagated to + a different thread, than the one that is running the async loop. + """ + + # Task attribute used to set/get the context + _CONTEXT_ATTR = "__datadog_context" + + def activate(self, context, loop=None): + """Sets the scoped ``Context`` for the current running ``Task``.""" + loop = self._get_loop(loop) + if not loop: + super(AsyncioContextProvider, self).activate(context) + return context + + # the current unit of work (if tasks are used) + task = asyncio.Task.current_task(loop=loop) + if task: + setattr(task, self._CONTEXT_ATTR, context) + return context + + def _get_loop(self, loop=None): + """Helper to try and resolve the current loop""" + try: + return loop or asyncio.get_event_loop() + except RuntimeError: + # Detects if a loop is available in the current thread; + # DEV: This happens when a new thread is created from the out that is running the async loop + # DEV: It's possible that a different Executor is handling a different Thread that + # works with blocking code. In that case, we fallback to a thread-local Context. + pass + return None + + def _has_active_context(self, loop=None): + """Helper to determine if we have a currently active context""" + loop = self._get_loop(loop=loop) + if loop is None: + return super(AsyncioContextProvider, self)._has_active_context() + + # the current unit of work (if tasks are used) + task = asyncio.Task.current_task(loop=loop) + if task is None: + return False + + ctx = getattr(task, self._CONTEXT_ATTR, None) + return ctx is not None + + def active(self, loop=None): + """Returns the active context for the execution.""" + loop = self._get_loop(loop=loop) + if not loop: + return super(AsyncioContextProvider, self).active() + + # the current unit of work (if tasks are used) + task = asyncio.Task.current_task(loop=loop) + if task is None: + return None + ctx = getattr(task, self._CONTEXT_ATTR, None) + if isinstance(ctx, Span): + return self._update_active(ctx) + return ctx diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/wrappers.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/wrappers.py new file mode 100644 index 000000000..ddbabc4ec --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/asyncio/wrappers.py @@ -0,0 +1,25 @@ +from .compat import asyncio_current_task +from .provider import AsyncioContextProvider + + +def wrapped_create_task(wrapped, instance, args, kwargs): + """Wrapper for ``create_task(coro)`` that propagates the current active + ``Context`` to the new ``Task``. This function is useful to connect traces + of detached executions. + + Note: we can't just link the task contexts due to the following scenario: + * begin task A + * task A starts task B1..B10 + * finish task B1-B9 (B10 still on trace stack) + * task A starts task C + * now task C gets parented to task B10 since it's still on the stack, + however was not actually triggered by B10 + """ + new_task = wrapped(*args, **kwargs) + current_task = asyncio_current_task() + + ctx = getattr(current_task, AsyncioContextProvider._CONTEXT_ATTR, None) + if ctx: + setattr(new_task, AsyncioContextProvider._CONTEXT_ATTR, ctx) + + return new_task diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/boto/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/boto/__init__.py new file mode 100644 index 000000000..cb3daee37 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/boto/__init__.py @@ -0,0 +1,26 @@ +""" +Boto integration will trace all AWS calls made via boto2. +This integration is automatically patched when using ``patch_all()``:: + + import boto.ec2 + from ddtrace import patch + + # If not patched yet, you can patch boto specifically + patch(boto=True) + + # This will report spans with the default instrumentation + ec2 = boto.ec2.connect_to_region("us-west-2") + # Example of instrumented query + ec2.get_all_instances() +""" + +from ...utils.importlib import require_modules + + +required_modules = ["boto.connection"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + + __all__ = ["patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/boto/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/boto/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..4a00a3c6a Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/boto/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/boto/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/boto/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..418697696 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/boto/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/boto/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/boto/patch.py new file mode 100644 index 000000000..f3aa77689 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/boto/patch.py @@ -0,0 +1,176 @@ +import inspect + +import boto.connection + +from ddtrace import config +from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY +from ddtrace.constants import SPAN_MEASURED_KEY +from ddtrace.ext import SpanTypes +from ddtrace.ext import aws +from ddtrace.ext import http +from ddtrace.pin import Pin +from ddtrace.utils.wrappers import unwrap +from ddtrace.vendor import wrapt + +from ...utils import get_argument_value + + +# Original boto client class +_Boto_client = boto.connection.AWSQueryConnection + +AWS_QUERY_ARGS_NAME = ("operation_name", "params", "path", "verb") +AWS_AUTH_ARGS_NAME = ( + "method", + "path", + "headers", + "data", + "host", + "auth_path", + "sender", +) +AWS_QUERY_TRACED_ARGS = {"operation_name", "params", "path"} +AWS_AUTH_TRACED_ARGS = {"path", "data", "host"} + + +def patch(): + if getattr(boto.connection, "_datadog_patch", False): + return + setattr(boto.connection, "_datadog_patch", True) + + # AWSQueryConnection and AWSAuthConnection are two different classes called by + # different services for connection. + # For example EC2 uses AWSQueryConnection and S3 uses AWSAuthConnection + wrapt.wrap_function_wrapper("boto.connection", "AWSQueryConnection.make_request", patched_query_request) + wrapt.wrap_function_wrapper("boto.connection", "AWSAuthConnection.make_request", patched_auth_request) + Pin(service="aws", app="aws").onto(boto.connection.AWSQueryConnection) + Pin(service="aws", app="aws").onto(boto.connection.AWSAuthConnection) + + +def unpatch(): + if getattr(boto.connection, "_datadog_patch", False): + setattr(boto.connection, "_datadog_patch", False) + unwrap(boto.connection.AWSQueryConnection, "make_request") + unwrap(boto.connection.AWSAuthConnection, "make_request") + + +# ec2, sqs, kinesis +def patched_query_request(original_func, instance, args, kwargs): + + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return original_func(*args, **kwargs) + + endpoint_name = getattr(instance, "host").split(".")[0] + + with pin.tracer.trace( + "{}.command".format(endpoint_name), + service="{}.{}".format(pin.service, endpoint_name), + span_type=SpanTypes.HTTP, + ) as span: + span.set_tag(SPAN_MEASURED_KEY) + + operation_name = None + if args: + operation_name = get_argument_value(args, kwargs, 0, "action") + span.resource = "%s.%s" % (endpoint_name, operation_name.lower()) + else: + span.resource = endpoint_name + + aws.add_span_arg_tags(span, endpoint_name, args, AWS_QUERY_ARGS_NAME, AWS_QUERY_TRACED_ARGS) + + # Obtaining region name + region_name = _get_instance_region_name(instance) + + meta = { + aws.AGENT: "boto", + aws.OPERATION: operation_name, + } + if region_name: + meta[aws.REGION] = region_name + + span.set_tags(meta) + + # Original func returns a boto.connection.HTTPResponse object + result = original_func(*args, **kwargs) + span.set_tag(http.STATUS_CODE, getattr(result, "status")) + span.set_tag(http.METHOD, getattr(result, "_method")) + + # set analytics sample rate + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.boto.get_analytics_sample_rate()) + + return result + + +# s3, lambda +def patched_auth_request(original_func, instance, args, kwargs): + + # Catching the name of the operation that called make_request() + operation_name = None + + # Go up the stack until we get the first non-ddtrace module + # DEV: For `lambda.list_functions()` this should be: + # - ddtrace.contrib.boto.patch + # - ddtrace.vendor.wrapt.wrappers + # - boto.awslambda.layer1 (make_request) + # - boto.awslambda.layer1 (list_functions) + # But can vary depending on Python versions; that's why we use an heuristic + frame = inspect.currentframe().f_back + operation_name = None + while frame: + if frame.f_code.co_name == "make_request": + operation_name = frame.f_back.f_code.co_name + break + frame = frame.f_back + + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return original_func(*args, **kwargs) + + endpoint_name = getattr(instance, "host").split(".")[0] + + with pin.tracer.trace( + "{}.command".format(endpoint_name), + service="{}.{}".format(pin.service, endpoint_name), + span_type=SpanTypes.HTTP, + ) as span: + span.set_tag(SPAN_MEASURED_KEY) + if args: + http_method = get_argument_value(args, kwargs, 0, "method") + span.resource = "%s.%s" % (endpoint_name, http_method.lower()) + else: + span.resource = endpoint_name + + aws.add_span_arg_tags(span, endpoint_name, args, AWS_AUTH_ARGS_NAME, AWS_AUTH_TRACED_ARGS) + + # Obtaining region name + region_name = _get_instance_region_name(instance) + + meta = { + aws.AGENT: "boto", + aws.OPERATION: operation_name, + } + if region_name: + meta[aws.REGION] = region_name + + span.set_tags(meta) + + # Original func returns a boto.connection.HTTPResponse object + result = original_func(*args, **kwargs) + span.set_tag(http.STATUS_CODE, getattr(result, "status")) + span.set_tag(http.METHOD, getattr(result, "_method")) + + # set analytics sample rate + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.boto.get_analytics_sample_rate()) + + return result + + +def _get_instance_region_name(instance): + region = getattr(instance, "region", None) + + if not region: + return None + if isinstance(region, str): + return region.split(":")[1] + else: + return region.name diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/botocore/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/botocore/__init__.py new file mode 100644 index 000000000..c60c8fa80 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/botocore/__init__.py @@ -0,0 +1,59 @@ +""" +The Botocore integration will trace all AWS calls made with the botocore +library. Libraries like Boto3 that use Botocore will also be patched. + +Enabling +~~~~~~~~ + +The botocore integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(botocore=True) + +Configuration +~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.botocore['distributed_tracing'] + + Whether to inject distributed tracing data to requests in SQS and Lambda. + + Can also be enabled with the ``DD_BOTOCORE_DISTRIBUTED_TRACING`` environment variable. + + Default: ``True`` + +.. py:data:: ddtrace.config.botocore['invoke_with_legacy_context'] + + This preserves legacy behavior when tracing directly invoked Python and Node Lambda + functions instrumented with datadog-lambda-python < v41 or datadog-lambda-js < v3.58.0. + + Legacy support for older libraries is available with + ``ddtrace.config.botocore.invoke_with_legacy_context = True`` or by setting the environment + variable ``DD_BOTOCORE_INVOKE_WITH_LEGACY_CONTEXT=true``. + + + Default: ``False`` + + +Example:: + + from ddtrace import config + + # Enable distributed tracing + config.botocore['distributed_tracing'] = True + +""" + + +from ...utils.importlib import require_modules + + +required_modules = ["botocore.client"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + + __all__ = ["patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/botocore/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/botocore/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..7e0320c1d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/botocore/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/botocore/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/botocore/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..0dcf92061 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/botocore/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/botocore/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/botocore/patch.py new file mode 100644 index 000000000..475fd778f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/botocore/patch.py @@ -0,0 +1,177 @@ +""" +Trace queries to aws api done via botocore client +""" +# 3p +import base64 +import json + +import botocore.client + +from ddtrace import config +from ddtrace.vendor import wrapt + +# project +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import aws +from ...ext import http +from ...internal.logger import get_logger +from ...pin import Pin +from ...propagation.http import HTTPPropagator +from ...utils import get_argument_value +from ...utils.formats import asbool +from ...utils.formats import deep_getattr +from ...utils.formats import get_env +from ...utils.wrappers import unwrap + + +# Original botocore client class +_Botocore_client = botocore.client.BaseClient + +ARGS_NAME = ("action", "params", "path", "verb") +TRACED_ARGS = {"params", "path", "verb"} + +log = get_logger(__name__) + +# Botocore default settings +config._add( + "botocore", + { + "distributed_tracing": asbool(get_env("botocore", "distributed_tracing", default=True)), + "invoke_with_legacy_context": asbool(get_env("botocore", "invoke_with_legacy_context", default=False)), + }, +) + + +def inject_trace_data_to_message_attributes(trace_data, entry): + if "MessageAttributes" not in entry: + entry["MessageAttributes"] = {} + # An Amazon SQS message can contain up to 10 metadata attributes. + if len(entry["MessageAttributes"]) < 10: + entry["MessageAttributes"]["_datadog"] = {"DataType": "String", "StringValue": json.dumps(trace_data)} + else: + log.debug("skipping trace injection, max number (10) of MessageAttributes exceeded") + + +def inject_trace_to_sqs_batch_message(args, span): + trace_data = {} + HTTPPropagator.inject(span.context, trace_data) + params = args[1] + + for entry in params["Entries"]: + inject_trace_data_to_message_attributes(trace_data, entry) + + +def inject_trace_to_sqs_message(args, span): + trace_data = {} + HTTPPropagator.inject(span.context, trace_data) + params = args[1] + + inject_trace_data_to_message_attributes(trace_data, params) + + +def modify_client_context(client_context_object, trace_headers): + if config.botocore["invoke_with_legacy_context"]: + trace_headers = {"_datadog": trace_headers} + + if "custom" in client_context_object: + client_context_object["custom"].update(trace_headers) + else: + client_context_object["custom"] = trace_headers + + +def inject_trace_to_client_context(args, span): + trace_headers = {} + HTTPPropagator.inject(span.context, trace_headers) + client_context_object = {} + params = args[1] + if "ClientContext" in params: + try: + client_context_json = base64.b64decode(params["ClientContext"]).decode("utf-8") + client_context_object = json.loads(client_context_json) + except Exception: + log.warning("malformed client_context=%s", params["ClientContext"], exc_info=True) + return + modify_client_context(client_context_object, trace_headers) + try: + json_context = json.dumps(client_context_object).encode("utf-8") + except Exception: + log.warning("unable to encode modified client context as json: %s", client_context_object, exc_info=True) + return + params["ClientContext"] = base64.b64encode(json_context).decode("utf-8") + + +def patch(): + if getattr(botocore.client, "_datadog_patch", False): + return + setattr(botocore.client, "_datadog_patch", True) + + wrapt.wrap_function_wrapper("botocore.client", "BaseClient._make_api_call", patched_api_call) + Pin(service="aws", app="aws").onto(botocore.client.BaseClient) + + +def unpatch(): + if getattr(botocore.client, "_datadog_patch", False): + setattr(botocore.client, "_datadog_patch", False) + unwrap(botocore.client.BaseClient, "_make_api_call") + + +def patched_api_call(original_func, instance, args, kwargs): + + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return original_func(*args, **kwargs) + + endpoint_name = deep_getattr(instance, "_endpoint._endpoint_prefix") + + with pin.tracer.trace( + "{}.command".format(endpoint_name), service="{}.{}".format(pin.service, endpoint_name), span_type=SpanTypes.HTTP + ) as span: + span.set_tag(SPAN_MEASURED_KEY) + operation = None + if args: + operation = get_argument_value(args, kwargs, 0, "operation_name") + # DEV: join is the fastest way of concatenating strings that is compatible + # across Python versions (see + # https://stackoverflow.com/questions/1316887/what-is-the-most-efficient-string-concatenation-method-in-python) + span.resource = ".".join((endpoint_name, operation.lower())) + + if config.botocore["distributed_tracing"]: + if endpoint_name == "lambda" and operation == "Invoke": + inject_trace_to_client_context(args, span) + if endpoint_name == "sqs" and operation == "SendMessage": + inject_trace_to_sqs_message(args, span) + if endpoint_name == "sqs" and operation == "SendMessageBatch": + inject_trace_to_sqs_batch_message(args, span) + + else: + span.resource = endpoint_name + + aws.add_span_arg_tags(span, endpoint_name, args, ARGS_NAME, TRACED_ARGS) + + region_name = deep_getattr(instance, "meta.region_name") + + span._set_str_tag("aws.agent", "botocore") + if operation is not None: + span._set_str_tag("aws.operation", operation) + if region_name is not None: + span._set_str_tag("aws.region", region_name) + + result = original_func(*args, **kwargs) + + response_meta = result.get("ResponseMetadata") + if response_meta: + if "HTTPStatusCode" in response_meta: + span.set_tag(http.STATUS_CODE, response_meta["HTTPStatusCode"]) + + if "RetryAttempts" in response_meta: + span.set_tag("retry_attempts", response_meta["RetryAttempts"]) + + if "RequestId" in response_meta: + span.set_tag("aws.requestid", response_meta["RequestId"]) + + # set analytics sample rate + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.botocore.get_analytics_sample_rate()) + + return result diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/__init__.py new file mode 100644 index 000000000..78d5562af --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/__init__.py @@ -0,0 +1,46 @@ +""" +The bottle integration traces the Bottle web framework. Add the following +plugin to your app:: + + import bottle + from ddtrace import tracer + from ddtrace.contrib.bottle import TracePlugin + + app = bottle.Bottle() + plugin = TracePlugin(service="my-web-app") + app.install(plugin) + +:ref:`All HTTP tags ` are supported for this integration. + +Configuration +~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.bottle['distributed_tracing'] + + Whether to parse distributed tracing headers from requests received by your bottle app. + + Can also be enabled with the ``DD_BOTTLE_DISTRIBUTED_TRACING`` environment variable. + + Default: ``True`` + + +Example:: + + from ddtrace import config + + # Enable distributed tracing + config.bottle['distributed_tracing'] = True + +""" + +from ...utils.importlib import require_modules + + +required_modules = ["bottle"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .trace import TracePlugin + + __all__ = ["TracePlugin", "patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..729b4a7a5 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..e9e0e5e4d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/__pycache__/trace.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/__pycache__/trace.cpython-38.pyc new file mode 100644 index 000000000..a82e0699d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/__pycache__/trace.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/patch.py new file mode 100644 index 000000000..7ec581357 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/patch.py @@ -0,0 +1,35 @@ +import bottle + +from ddtrace import config +from ddtrace.vendor import wrapt + +from ...utils.formats import asbool +from ...utils.formats import get_env +from .trace import TracePlugin + + +# Configure default configuration +config._add( + "bottle", + dict( + distributed_tracing=asbool(get_env("bottle", "distributed_tracing", default=True)), + ), +) + + +def patch(): + """Patch the bottle.Bottle class""" + if getattr(bottle, "_datadog_patch", False): + return + + setattr(bottle, "_datadog_patch", True) + wrapt.wrap_function_wrapper("bottle", "Bottle.__init__", traced_init) + + +def traced_init(wrapped, instance, args, kwargs): + wrapped(*args, **kwargs) + + service = config._get_service(default="bottle") + + plugin = TracePlugin(service=service) + instance.install(plugin) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/trace.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/trace.py new file mode 100644 index 000000000..e0b5950a6 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/bottle/trace.py @@ -0,0 +1,95 @@ +from bottle import HTTPError +from bottle import HTTPResponse +from bottle import request +from bottle import response + +import ddtrace +from ddtrace import config + +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...utils.formats import asbool + + +class TracePlugin(object): + name = "trace" + api = 2 + + def __init__(self, service="bottle", tracer=None, distributed_tracing=None): + self.service = config.service or service + self.tracer = tracer or ddtrace.tracer + if distributed_tracing is not None: + config.bottle.distributed_tracing = distributed_tracing + + @property + def distributed_tracing(self): + return config.bottle.distributed_tracing + + @distributed_tracing.setter + def distributed_tracing(self, distributed_tracing): + config.bottle["distributed_tracing"] = asbool(distributed_tracing) + + def apply(self, callback, route): + def wrapped(*args, **kwargs): + if not self.tracer or not self.tracer.enabled: + return callback(*args, **kwargs) + + resource = "{} {}".format(request.method, route.rule) + + trace_utils.activate_distributed_headers( + self.tracer, int_config=config.bottle, request_headers=request.headers + ) + + with self.tracer.trace( + "bottle.request", + service=self.service, + resource=resource, + span_type=SpanTypes.WEB, + ) as s: + s.set_tag(SPAN_MEASURED_KEY) + # set analytics sample rate with global config enabled + s.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.bottle.get_analytics_sample_rate(use_global_config=True)) + + code = None + result = None + try: + result = callback(*args, **kwargs) + return result + except (HTTPError, HTTPResponse) as e: + # you can interrupt flows using abort(status_code, 'message')... + # we need to respect the defined status_code. + # we also need to handle when response is raised as is the + # case with a 4xx status + code = e.status_code + raise + except Exception: + # bottle doesn't always translate unhandled exceptions, so + # we mark it here. + code = 500 + raise + finally: + if isinstance(result, HTTPResponse): + response_code = result.status_code + elif code: + response_code = code + else: + # bottle local response has not yet been updated so this + # will be default + response_code = response.status_code + + method = request.method + url = request.urlparts._replace(query="").geturl() + trace_utils.set_http_meta( + s, + config.bottle, + method=method, + url=url, + status_code=response_code, + query=request.query_string, + request_headers=request.headers, + response_headers=response.headers, + ) + + return wrapped diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/__init__.py new file mode 100644 index 000000000..f0718e1a8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/__init__.py @@ -0,0 +1,37 @@ +"""Instrument Cassandra to report Cassandra queries. + +``patch_all`` will automatically patch your Cluster instance to make it work. +:: + + from ddtrace import Pin, patch + from cassandra.cluster import Cluster + + # If not patched yet, you can patch cassandra specifically + patch(cassandra=True) + + # This will report spans with the default instrumentation + cluster = Cluster(contact_points=["127.0.0.1"], port=9042) + session = cluster.connect("my_keyspace") + # Example of instrumented query + session.execute("select id from my_table limit 10;") + + # Use a pin to specify metadata related to this cluster + cluster = Cluster(contact_points=['10.1.1.3', '10.1.1.4', '10.1.1.5'], port=9042) + Pin.override(cluster, service='cassandra-backend') + session = cluster.connect("my_keyspace") + session.execute("select id from my_table limit 10;") +""" +from ...utils.importlib import require_modules + + +required_modules = ["cassandra.cluster"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .session import get_traced_cassandra + from .session import patch + + __all__ = [ + "get_traced_cassandra", + "patch", + ] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..3d9151252 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..f3562917f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/__pycache__/session.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/__pycache__/session.cpython-38.pyc new file mode 100644 index 000000000..a5b11cef0 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/__pycache__/session.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/patch.py new file mode 100644 index 000000000..d82e620f7 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/patch.py @@ -0,0 +1,5 @@ +from .session import patch +from .session import unpatch + + +__all__ = ["patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/session.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/session.py new file mode 100644 index 000000000..8aab1d08f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cassandra/session.py @@ -0,0 +1,291 @@ +""" +Trace queries along a session to a cassandra cluster +""" +import sys + +# 3p +import cassandra.cluster + +# project +from ddtrace import config + +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import ERROR_MSG +from ...constants import ERROR_TYPE +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import cassandra as cassx +from ...ext import net +from ...internal.compat import maybe_stringify +from ...internal.compat import stringify +from ...internal.logger import get_logger +from ...pin import Pin +from ...utils import get_argument_value +from ...utils.deprecation import deprecated +from ...utils.formats import deep_getattr +from ...vendor import wrapt + + +log = get_logger(__name__) + +RESOURCE_MAX_LENGTH = 5000 +SERVICE = "cassandra" +CURRENT_SPAN = "_ddtrace_current_span" +PAGE_NUMBER = "_ddtrace_page_number" + +# Original connect connect function +_connect = cassandra.cluster.Cluster.connect + + +def patch(): + """patch will add tracing to the cassandra library.""" + setattr(cassandra.cluster.Cluster, "connect", wrapt.FunctionWrapper(_connect, traced_connect)) + Pin(service=SERVICE, app=SERVICE).onto(cassandra.cluster.Cluster) + + +def unpatch(): + cassandra.cluster.Cluster.connect = _connect + + +def traced_connect(func, instance, args, kwargs): + session = func(*args, **kwargs) + if not isinstance(session.execute, wrapt.FunctionWrapper): + # FIXME[matt] this should probably be private. + setattr(session, "execute_async", wrapt.FunctionWrapper(session.execute_async, traced_execute_async)) + return session + + +def _close_span_on_success(result, future): + span = getattr(future, CURRENT_SPAN, None) + if not span: + log.debug("traced_set_final_result was not able to get the current span from the ResponseFuture") + return + try: + span.set_tags(_extract_result_metas(cassandra.cluster.ResultSet(future, result))) + except Exception: + log.debug("an exception occurred while setting tags", exc_info=True) + finally: + span.finish() + delattr(future, CURRENT_SPAN) + + +def traced_set_final_result(func, instance, args, kwargs): + result = get_argument_value(args, kwargs, 0, "response") + _close_span_on_success(result, instance) + return func(*args, **kwargs) + + +def _close_span_on_error(exc, future): + span = getattr(future, CURRENT_SPAN, None) + if not span: + log.debug("traced_set_final_exception was not able to get the current span from the ResponseFuture") + return + try: + # handling the exception manually because we + # don't have an ongoing exception here + span.error = 1 + span.set_tag(ERROR_MSG, exc.args[0]) + span.set_tag(ERROR_TYPE, exc.__class__.__name__) + except Exception: + log.debug("traced_set_final_exception was not able to set the error, failed with error", exc_info=True) + finally: + span.finish() + delattr(future, CURRENT_SPAN) + + +def traced_set_final_exception(func, instance, args, kwargs): + exc = get_argument_value(args, kwargs, 0, "response") + _close_span_on_error(exc, instance) + return func(*args, **kwargs) + + +def traced_start_fetching_next_page(func, instance, args, kwargs): + has_more_pages = getattr(instance, "has_more_pages", True) + if not has_more_pages: + return func(*args, **kwargs) + session = getattr(instance, "session", None) + cluster = getattr(session, "cluster", None) + pin = Pin.get_from(cluster) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + # In case the current span is not finished we make sure to finish it + old_span = getattr(instance, CURRENT_SPAN, None) + if old_span: + log.debug("previous span was not finished before fetching next page") + old_span.finish() + + query = getattr(instance, "query", None) + + span = _start_span_and_set_tags(pin, query, session, cluster) + + page_number = getattr(instance, PAGE_NUMBER, 1) + 1 + setattr(instance, PAGE_NUMBER, page_number) + setattr(instance, CURRENT_SPAN, span) + try: + return func(*args, **kwargs) + except Exception: + with span: + span.set_exc_info(*sys.exc_info()) + raise + + +def traced_execute_async(func, instance, args, kwargs): + cluster = getattr(instance, "cluster", None) + pin = Pin.get_from(cluster) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + query = get_argument_value(args, kwargs, 0, "query") + + span = _start_span_and_set_tags(pin, query, instance, cluster) + + try: + result = func(*args, **kwargs) + setattr(result, CURRENT_SPAN, span) + setattr(result, PAGE_NUMBER, 1) + setattr(result, "_set_final_result", wrapt.FunctionWrapper(result._set_final_result, traced_set_final_result)) + setattr( + result, + "_set_final_exception", + wrapt.FunctionWrapper(result._set_final_exception, traced_set_final_exception), + ) + setattr( + result, + "start_fetching_next_page", + wrapt.FunctionWrapper(result.start_fetching_next_page, traced_start_fetching_next_page), + ) + # Since we cannot be sure that the previous methods were overwritten + # before the call ended, we add callbacks that will be run + # synchronously if the call already returned and we remove them right + # after. + result.add_callbacks( + _close_span_on_success, _close_span_on_error, callback_args=(result,), errback_args=(result,) + ) + result.clear_callbacks() + return result + except Exception: + with span: + span.set_exc_info(*sys.exc_info()) + raise + + +def _start_span_and_set_tags(pin, query, session, cluster): + service = pin.service + tracer = pin.tracer + span = tracer.trace("cassandra.query", service=service, span_type=SpanTypes.CASSANDRA) + span.set_tag(SPAN_MEASURED_KEY) + _sanitize_query(span, query) + span.set_tags(_extract_session_metas(session)) # FIXME[matt] do once? + span.set_tags(_extract_cluster_metas(cluster)) + # set analytics sample rate if enabled + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.cassandra.get_analytics_sample_rate()) + return span + + +def _extract_session_metas(session): + metas = {} + + if getattr(session, "keyspace", None): + # FIXME the keyspace can be overridden explicitly in the query itself + # e.g. 'select * from trace.hash_to_resource' + metas[cassx.KEYSPACE] = session.keyspace.lower() + + return metas + + +def _extract_cluster_metas(cluster): + metas = {} + if deep_getattr(cluster, "metadata.cluster_name"): + metas[cassx.CLUSTER] = cluster.metadata.cluster_name + if getattr(cluster, "port", None): + metas[net.TARGET_PORT] = cluster.port + + return metas + + +def _extract_result_metas(result): + metas = {} + if result is None: + return metas + + future = getattr(result, "response_future", None) + + if future: + # get the host + host = maybe_stringify(getattr(future, "coordinator_host", None)) + if host: + host, _, port = host.partition(":") + metas[net.TARGET_HOST] = host + if port: + metas[net.TARGET_PORT] = int(port) + elif hasattr(future, "_current_host"): + address = deep_getattr(future, "_current_host.address") + if address: + metas[net.TARGET_HOST] = address + + query = getattr(future, "query", None) + if getattr(query, "consistency_level", None): + metas[cassx.CONSISTENCY_LEVEL] = query.consistency_level + if getattr(query, "keyspace", None): + metas[cassx.KEYSPACE] = query.keyspace.lower() + + page_number = getattr(future, PAGE_NUMBER, 1) + has_more_pages = getattr(future, "has_more_pages") + is_paginated = has_more_pages or page_number > 1 + metas[cassx.PAGINATED] = is_paginated + if is_paginated: + metas[cassx.PAGE_NUMBER] = page_number + + if hasattr(result, "current_rows"): + result_rows = result.current_rows or [] + metas[cassx.ROW_COUNT] = len(result_rows) + + return metas + + +def _sanitize_query(span, query): + # TODO (aaditya): fix this hacky type check. we need it to avoid circular imports + t = type(query).__name__ + + resource = None + if t in ("SimpleStatement", "PreparedStatement"): + # reset query if a string is available + resource = getattr(query, "query_string", query) + elif t == "BatchStatement": + resource = "BatchStatement" + # Each element in `_statements_and_parameters` is: + # (is_prepared, statement, parameters) + # ref:https://github.com/datastax/python-driver/blob/13d6d72be74f40fcef5ec0f2b3e98538b3b87459/cassandra/query.py#L844 + # + # For prepared statements, the `statement` value is just the query_id + # which is not a statement and when trying to join with other strings + # raises an error in python3 around joining bytes to unicode, so this + # just filters out prepared statements from this tag value + q = "; ".join(q[1] for q in query._statements_and_parameters[:2] if not q[0]) + span.set_tag("cassandra.query", q) + span.set_metric("cassandra.batch_size", len(query._statements_and_parameters)) + elif t == "BoundStatement": + ps = getattr(query, "prepared_statement", None) + if ps: + resource = getattr(ps, "query_string", None) + elif t == "str": + resource = query + else: + resource = "unknown-query-type" # FIXME[matt] what else do to here? + + span.resource = stringify(resource)[:RESOURCE_MAX_LENGTH] + + +# +# DEPRECATED +# + + +@deprecated(message="Use patching instead (see the docs).", version="1.0.0") +def get_traced_cassandra(*args, **kwargs): + return _get_traced_cluster(*args, **kwargs) + + +def _get_traced_cluster(*args, **kwargs): + return cassandra.cluster.Cluster diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__init__.py new file mode 100644 index 000000000..1155d74b2 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__init__.py @@ -0,0 +1,66 @@ +""" +The Celery integration will trace all tasks that are executed in the +background. Functions and class based tasks are traced only if the Celery API +is used, so calling the function directly or via the ``run()`` method will not +generate traces. However, calling ``apply()``, ``apply_async()`` and ``delay()`` +will produce tracing data. To trace your Celery application, call the patch method:: + + import celery + from ddtrace import patch + + patch(celery=True) + app = celery.Celery() + + @app.task + def my_task(): + pass + + class MyTask(app.Task): + def run(self): + pass + +Configuration +~~~~~~~~~~~~~ +.. py:data:: ddtrace.config.celery['distributed_tracing'] + + Whether or not to pass distributed tracing headers to Celery workers. + + Can also be enabled with the ``DD_CELERY_DISTRIBUTED_TRACING`` environment variable. + + Default: ``False`` + +.. py:data:: ddtrace.config.celery['producer_service_name'] + + Sets service name for producer + + Default: ``'celery-producer'`` + +.. py:data:: ddtrace.config.celery['worker_service_name'] + + Sets service name for worker + + Default: ``'celery-worker'`` + +""" +from ...utils.importlib import require_modules + + +required_modules = ["celery"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .app import patch_app + from .app import unpatch_app + from .patch import patch + from .patch import unpatch + from .task import patch_task + from .task import unpatch_task + + __all__ = [ + "patch", + "patch_app", + "patch_task", + "unpatch", + "unpatch_app", + "unpatch_task", + ] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..b974e2daa Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/app.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/app.cpython-38.pyc new file mode 100644 index 000000000..85f09d80f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/app.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/constants.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/constants.cpython-38.pyc new file mode 100644 index 000000000..8849e0e36 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/constants.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..ee10f11c8 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/signals.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/signals.cpython-38.pyc new file mode 100644 index 000000000..c7fa6752d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/signals.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/task.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/task.cpython-38.pyc new file mode 100644 index 000000000..8110c37a8 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/task.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/utils.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/utils.cpython-38.pyc new file mode 100644 index 000000000..e329726c8 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/__pycache__/utils.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/app.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/app.py new file mode 100644 index 000000000..3a11b8e76 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/app.py @@ -0,0 +1,59 @@ +from celery import signals + +from ddtrace import Pin +from ddtrace import config +from ddtrace.pin import _DD_PIN_NAME + +from .constants import APP +from .signals import trace_after_publish +from .signals import trace_before_publish +from .signals import trace_failure +from .signals import trace_postrun +from .signals import trace_prerun +from .signals import trace_retry + + +def patch_app(app, pin=None): + """Attach the Pin class to the application and connect + our handlers to Celery signals. + """ + if getattr(app, "__datadog_patch", False): + return + setattr(app, "__datadog_patch", True) + + # attach the PIN object + pin = pin or Pin( + service=config.celery["worker_service_name"], + app=APP, + _config=config.celery, + ) + pin.onto(app) + # connect to the Signal framework + + signals.task_prerun.connect(trace_prerun, weak=False) + signals.task_postrun.connect(trace_postrun, weak=False) + signals.before_task_publish.connect(trace_before_publish, weak=False) + signals.after_task_publish.connect(trace_after_publish, weak=False) + signals.task_failure.connect(trace_failure, weak=False) + signals.task_retry.connect(trace_retry, weak=False) + return app + + +def unpatch_app(app): + """Remove the Pin instance from the application and disconnect + our handlers from Celery signal framework. + """ + if not getattr(app, "__datadog_patch", False): + return + setattr(app, "__datadog_patch", False) + + pin = Pin.get_from(app) + if pin is not None: + delattr(app, _DD_PIN_NAME) + + signals.task_prerun.disconnect(trace_prerun) + signals.task_postrun.disconnect(trace_postrun) + signals.before_task_publish.disconnect(trace_before_publish) + signals.after_task_publish.disconnect(trace_after_publish) + signals.task_failure.disconnect(trace_failure) + signals.task_retry.disconnect(trace_retry) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/constants.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/constants.py new file mode 100644 index 000000000..496f9ce2e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/constants.py @@ -0,0 +1,21 @@ +from ddtrace import config + + +# Celery Context key +CTX_KEY = "__dd_task_span" + +# Span names +PRODUCER_ROOT_SPAN = "celery.apply" +WORKER_ROOT_SPAN = "celery.run" + +# Task operations +TASK_TAG_KEY = "celery.action" +TASK_APPLY = "apply" +TASK_APPLY_ASYNC = "apply_async" +TASK_RUN = "run" +TASK_RETRY_REASON_KEY = "celery.retry.reason" + +# Service info +APP = "celery" +PRODUCER_SERVICE = config._get_service(default="celery-producer") +WORKER_SERVICE = config._get_service(default="celery-worker") diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/patch.py new file mode 100644 index 000000000..7a4402918 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/patch.py @@ -0,0 +1,38 @@ +import celery + +from ddtrace import config +import ddtrace.internal.forksafe as forksafe + +from ...utils.formats import get_env +from .app import patch_app +from .app import unpatch_app +from .constants import PRODUCER_SERVICE +from .constants import WORKER_SERVICE + + +forksafe._soft = True + + +# Celery default settings +config._add( + "celery", + { + "distributed_tracing": get_env("celery", "distributed_tracing", default=False), + "producer_service_name": get_env("celery", "producer_service_name", default=PRODUCER_SERVICE), + "worker_service_name": get_env("celery", "worker_service_name", default=WORKER_SERVICE), + }, +) + + +def patch(): + """Instrument Celery base application and the `TaskRegistry` so + that any new registered task is automatically instrumented. In the + case of Django-Celery integration, also the `@shared_task` decorator + must be instrumented because Django doesn't use the Celery registry. + """ + patch_app(celery.Celery) + + +def unpatch(): + """Disconnect all signals and remove Tracing capabilities""" + unpatch_app(celery.Celery) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/signals.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/signals.py new file mode 100644 index 000000000..3b6be094f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/signals.py @@ -0,0 +1,193 @@ +from celery import registry + +from ddtrace import Pin +from ddtrace import config + +from . import constants as c +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...internal.logger import get_logger +from ...propagation.http import HTTPPropagator +from .utils import attach_span +from .utils import detach_span +from .utils import retrieve_span +from .utils import retrieve_task_id +from .utils import set_tags_from_context + + +log = get_logger(__name__) +propagator = HTTPPropagator + + +def trace_prerun(*args, **kwargs): + # safe-guard to avoid crashes in case the signals API + # changes in Celery + task = kwargs.get("sender") + task_id = kwargs.get("task_id") + log.debug("prerun signal start task_id=%s", task_id) + if task is None or task_id is None: + log.debug("unable to extract the Task and the task_id. This version of Celery may not be supported.") + return + + # retrieve the task Pin or fallback to the global one + pin = Pin.get_from(task) or Pin.get_from(task.app) + if pin is None: + log.debug("no pin found on task or task.app task_id=%s", task_id) + return + + request_headers = task.request.get("headers", {}) + trace_utils.activate_distributed_headers(pin.tracer, int_config=config.celery, request_headers=request_headers) + + # propagate the `Span` in the current task Context + service = config.celery["worker_service_name"] + span = pin.tracer.trace(c.WORKER_ROOT_SPAN, service=service, resource=task.name, span_type=SpanTypes.WORKER) + # set analytics sample rate + rate = config.celery.get_analytics_sample_rate() + if rate is not None: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, rate) + + span.set_tag(SPAN_MEASURED_KEY) + attach_span(task, task_id, span) + + +def trace_postrun(*args, **kwargs): + # safe-guard to avoid crashes in case the signals API + # changes in Celery + task = kwargs.get("sender") + task_id = kwargs.get("task_id") + log.debug("postrun signal task_id=%s", task_id) + if task is None or task_id is None: + log.debug("unable to extract the Task and the task_id. This version of Celery may not be supported.") + return + + # retrieve and finish the Span + span = retrieve_span(task, task_id) + if span is None: + log.warning("no existing span found for task_id=%s", task_id) + return + else: + # request context tags + span.set_tag(c.TASK_TAG_KEY, c.TASK_RUN) + set_tags_from_context(span, kwargs) + set_tags_from_context(span, task.request.__dict__) + span.finish() + detach_span(task, task_id) + + +def trace_before_publish(*args, **kwargs): + # `before_task_publish` signal doesn't propagate the task instance so + # we need to retrieve it from the Celery Registry to access the `Pin`. The + # `Task` instance **does not** include any information about the current + # execution, so it **must not** be used to retrieve `request` data. + task_name = kwargs.get("sender") + task = registry.tasks.get(task_name) + task_id = retrieve_task_id(kwargs) + # safe-guard to avoid crashes in case the signals API + # changes in Celery + if task is None or task_id is None: + log.debug("unable to extract the Task and the task_id. This version of Celery may not be supported.") + return + + # propagate the `Span` in the current task Context + pin = Pin.get_from(task) or Pin.get_from(task.app) + if pin is None: + return + + # apply some tags here because most of the data is not available + # in the task_after_publish signal + service = config.celery["producer_service_name"] + span = pin.tracer.trace(c.PRODUCER_ROOT_SPAN, service=service, resource=task_name) + # set analytics sample rate + rate = config.celery.get_analytics_sample_rate() + if rate is not None: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, rate) + + span.set_tag(SPAN_MEASURED_KEY) + span.set_tag(c.TASK_TAG_KEY, c.TASK_APPLY_ASYNC) + span.set_tag("celery.id", task_id) + set_tags_from_context(span, kwargs) + + # Note: adding tags from `traceback` or `state` calls will make an + # API call to the backend for the properties so we should rely + # only on the given `Context` + attach_span(task, task_id, span, is_publish=True) + + if config.celery["distributed_tracing"]: + trace_headers = {} + propagator.inject(span.context, trace_headers) + + # This weirdness is due to yet another Celery bug concerning + # how headers get propagated in async flows + # https://github.com/celery/celery/issues/4875 + task_headers = kwargs.get("headers") or {} + task_headers.setdefault("headers", {}) + task_headers["headers"].update(trace_headers) + kwargs["headers"] = task_headers + + +def trace_after_publish(*args, **kwargs): + task_name = kwargs.get("sender") + task = registry.tasks.get(task_name) + task_id = retrieve_task_id(kwargs) + # safe-guard to avoid crashes in case the signals API + # changes in Celery + if task is None or task_id is None: + log.debug("unable to extract the Task and the task_id. This version of Celery may not be supported.") + return + + # retrieve and finish the Span + span = retrieve_span(task, task_id, is_publish=True) + if span is None: + return + else: + span.finish() + detach_span(task, task_id, is_publish=True) + + +def trace_failure(*args, **kwargs): + # safe-guard to avoid crashes in case the signals API + # changes in Celery + task = kwargs.get("sender") + task_id = kwargs.get("task_id") + if task is None or task_id is None: + log.debug("unable to extract the Task and the task_id. This version of Celery may not be supported.") + return + + # retrieve and finish the Span + span = retrieve_span(task, task_id) + if span is None: + return + else: + # add Exception tags; post signals are still called + # so we don't need to attach other tags here + ex = kwargs.get("einfo") + if ex is None: + return + if hasattr(task, "throws") and isinstance(ex.exception, task.throws): + return + span.set_exc_info(ex.type, ex.exception, ex.tb) + + +def trace_retry(*args, **kwargs): + # safe-guard to avoid crashes in case the signals API + # changes in Celery + task = kwargs.get("sender") + context = kwargs.get("request") + if task is None or context is None: + log.debug("unable to extract the Task or the Context. This version of Celery may not be supported.") + return + + reason = kwargs.get("reason") + if not reason: + log.debug("unable to extract the retry reason. This version of Celery may not be supported.") + return + + span = retrieve_span(task, context.id) + if span is None: + return + + # Add retry reason metadata to span + # DEV: Use `str(reason)` instead of `reason.message` in case we get something that isn't an `Exception` + span.set_tag(c.TASK_RETRY_REASON_KEY, str(reason)) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/task.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/task.py new file mode 100644 index 000000000..b73a63d5c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/task.py @@ -0,0 +1,31 @@ +from ...utils.deprecation import deprecation +from .app import patch_app + + +def patch_task(task, pin=None): + """Deprecated API. The new API uses signals that can be activated via + patch(celery=True) or through `ddtrace-run` script. Using this API + enables instrumentation on all tasks. + """ + deprecation( + name="ddtrace.contrib.celery.patch_task", + message="Use `patch(celery=True)` or `ddtrace-run` script instead", + version="1.0.0", + ) + + # Enable instrumentation everywhere + patch_app(task.app) + return task + + +def unpatch_task(task): + """Deprecated API. The new API uses signals that can be deactivated + via unpatch() API. This API is now a no-op implementation so it doesn't + affect instrumented tasks. + """ + deprecation( + name="ddtrace.contrib.celery.patch_task", + message="Use `unpatch()` instead", + version="1.0.0", + ) + return task diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/utils.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/utils.py new file mode 100644 index 000000000..b8c245198 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/celery/utils.py @@ -0,0 +1,126 @@ +from typing import Any +from typing import Dict +from weakref import WeakValueDictionary + +from ddtrace.span import Span + +from .constants import CTX_KEY + + +TAG_KEYS = frozenset( + [ + ("compression", "celery.compression"), + ("correlation_id", "celery.correlation_id"), + ("countdown", "celery.countdown"), + ("delivery_info", "celery.delivery_info"), + ("eta", "celery.eta"), + ("exchange", "celery.exchange"), + ("expires", "celery.expires"), + ("hostname", "celery.hostname"), + ("id", "celery.id"), + ("priority", "celery.priority"), + ("queue", "celery.queue"), + ("reply_to", "celery.reply_to"), + ("retries", "celery.retries"), + ("routing_key", "celery.routing_key"), + ("serializer", "celery.serializer"), + ("timelimit", "celery.timelimit"), + # Celery 4.0 uses `origin` instead of `hostname`; this change preserves + # the same name for the tag despite Celery version + ("origin", "celery.hostname"), + ("state", "celery.state"), + ] +) + + +def set_tags_from_context(span, context): + # type: (Span, Dict[str, Any]) -> None + """Helper to extract meta values from a Celery Context""" + + for key, tag_name in TAG_KEYS: + value = context.get(key) + + # Skip this key if it is not set + if value is None or value == "": + continue + + # Skip `timelimit` if it is not set (its default/unset value is a + # tuple or a list of `None` values + if key == "timelimit" and all(_ is None for _ in value): + continue + + # Skip `retries` if its value is `0` + if key == "retries" and value == 0: + continue + + span.set_tag(tag_name, value) + + +def attach_span(task, task_id, span, is_publish=False): + """Helper to propagate a `Span` for the given `Task` instance. This + function uses a `WeakValueDictionary` that stores a Datadog Span using + the `(task_id, is_publish)` as a key. This is useful when information must be + propagated from one Celery signal to another. + + DEV: We use (task_id, is_publish) for the key to ensure that publishing a + task from within another task does not cause any conflicts. + + This mostly happens when either a task fails and a retry policy is in place, + or when a task is manually retried (e.g. `task.retry()`), we end up trying + to publish a task with the same id as the task currently running. + + Previously publishing the new task would overwrite the existing `celery.run` span + in the `weak_dict` causing that span to be forgotten and never finished. + + NOTE: We cannot test for this well yet, because we do not run a celery worker, + and cannot run `task.apply_async()` + """ + weak_dict = getattr(task, CTX_KEY, None) + if weak_dict is None: + weak_dict = WeakValueDictionary() + setattr(task, CTX_KEY, weak_dict) + + weak_dict[(task_id, is_publish)] = span + + +def detach_span(task, task_id, is_publish=False): + """Helper to remove a `Span` in a Celery task when it's propagated. + This function handles tasks where the `Span` is not attached. + """ + weak_dict = getattr(task, CTX_KEY, None) + if weak_dict is None: + return + + # DEV: See note in `attach_span` for key info + try: + del weak_dict[(task_id, is_publish)] + except KeyError: + pass + + +def retrieve_span(task, task_id, is_publish=False): + """Helper to retrieve an active `Span` stored in a `Task` + instance + """ + weak_dict = getattr(task, CTX_KEY, None) + if weak_dict is None: + return + else: + # DEV: See note in `attach_span` for key info + return weak_dict.get((task_id, is_publish)) + + +def retrieve_task_id(context): + """Helper to retrieve the `Task` identifier from the message `body`. + This helper supports Protocol Version 1 and 2. The Protocol is well + detailed in the official documentation: + http://docs.celeryproject.org/en/latest/internals/protocol.html + """ + headers = context.get("headers") + body = context.get("body") + if headers: + # Protocol Version 2 (default from Celery 4.0) + return headers.get("id") + else: + # Protocol Version 1 + return body.get("id") diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cherrypy/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cherrypy/__init__.py new file mode 100644 index 000000000..6dc304aa0 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cherrypy/__init__.py @@ -0,0 +1,65 @@ +""" +The Cherrypy trace middleware will track request timings. +It uses the cherrypy hooks and creates a tool to track requests and errors + + +Usage +~~~~~ +To install the middleware, add:: + + from ddtrace import tracer + from ddtrace.contrib.cherrypy import TraceMiddleware + +and create a `TraceMiddleware` object:: + + traced_app = TraceMiddleware(cherrypy, tracer, service="my-cherrypy-app") + + +Configuration +~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.cherrypy['distributed_tracing'] + + Whether to parse distributed tracing headers from requests received by your CherryPy app. + + Can also be enabled with the ``DD_CHERRYPY_DISTRIBUTED_TRACING`` environment variable. + + Default: ``True`` + +.. py:data:: ddtrace.config.cherrypy['service'] + + The service name reported for your CherryPy app. + + Can also be configured via the ``DD_SERVICE`` environment variable. + + Default: ``'cherrypy'`` + + +Example:: +Here is the end result, in a sample app:: + + import cherrypy + + from ddtrace import tracer, Pin + from ddtrace.contrib.cherrypy import TraceMiddleware + TraceMiddleware(cherrypy, tracer, service="my-cherrypy-app") + + @cherrypy.tools.tracer() + class HelloWorld(object): + def index(self): + return "Hello World" + index.exposed = True + + cherrypy.quickstart(HelloWorld()) +""" + +from ..util import require_modules + + +required_modules = ["cherrypy"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .middleware import TraceMiddleware + + __all__ = ["TraceMiddleware"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cherrypy/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cherrypy/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..80d74ca51 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cherrypy/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cherrypy/__pycache__/middleware.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cherrypy/__pycache__/middleware.cpython-38.pyc new file mode 100644 index 000000000..48c5fcf45 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cherrypy/__pycache__/middleware.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cherrypy/middleware.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cherrypy/middleware.py new file mode 100644 index 000000000..9237d8fa3 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/cherrypy/middleware.py @@ -0,0 +1,146 @@ +""" +Datadog trace code for cherrypy. +""" + +# stdlib +import logging + +# 3p +import cherrypy +from cherrypy.lib.httputil import valid_status + +# project +from ddtrace import config +from ddtrace.constants import ERROR_MSG +from ddtrace.constants import ERROR_STACK +from ddtrace.constants import ERROR_TYPE + +from .. import trace_utils +from ...ext import SpanTypes +from ...internal import compat +from ...utils.formats import asbool +from ...utils.formats import get_env + + +log = logging.getLogger(__name__) + + +# Configure default configuration +config._add( + "cherrypy", + dict( + distributed_tracing=asbool(get_env("cherrypy", "distributed_tracing", default=True)), + ), +) + +SPAN_NAME = "cherrypy.request" + + +class TraceTool(cherrypy.Tool): + def __init__(self, app, tracer, service, use_distributed_tracing=None): + self.app = app + self._tracer = tracer + self.service = service + if use_distributed_tracing is not None: + self.use_distributed_tracing = use_distributed_tracing + + # CherryPy uses priority to determine which tools act first on each event. The lower the number, the higher + # the priority. See: https://docs.cherrypy.org/en/latest/extend.html#tools-ordering + cherrypy.Tool.__init__(self, "on_start_resource", self._on_start_resource, priority=95) + + @property + def use_distributed_tracing(self): + return config.cherrypy.distributed_tracing + + @use_distributed_tracing.setter + def use_distributed_tracing(self, use_distributed_tracing): + config.cherrypy["distributed_tracing"] = asbool(use_distributed_tracing) + + @property + def service(self): + return config.cherrypy.get("service", "cherrypy") + + @service.setter + def service(self, service): + config.cherrypy["service"] = service + + def _setup(self): + cherrypy.Tool._setup(self) + cherrypy.request.hooks.attach("on_end_request", self._on_end_request, priority=5) + cherrypy.request.hooks.attach("after_error_response", self._after_error_response, priority=5) + + def _on_start_resource(self): + trace_utils.activate_distributed_headers( + self._tracer, int_config=config.cherrypy, request_headers=cherrypy.request.headers + ) + + cherrypy.request._datadog_span = self._tracer.trace( + SPAN_NAME, + service=trace_utils.int_service(None, config.cherrypy, default="cherrypy"), + span_type=SpanTypes.WEB, + ) + + def _after_error_response(self): + span = getattr(cherrypy.request, "_datadog_span", None) + + if not span: + log.warning("cherrypy: tracing tool after_error_response hook called, but no active span found") + return + + span.error = 1 + span.set_tag(ERROR_TYPE, cherrypy._cperror._exc_info()[0]) + span.set_tag(ERROR_MSG, str(cherrypy._cperror._exc_info()[1])) + span.set_tag(ERROR_STACK, cherrypy._cperror.format_exc()) + + self._close_span(span) + + def _on_end_request(self): + span = getattr(cherrypy.request, "_datadog_span", None) + + if not span: + log.warning("cherrypy: tracing tool on_end_request hook called, but no active span found") + return + + self._close_span(span) + + def _close_span(self, span): + # Let users specify their own resource in middleware if they so desire. + # See case https://github.com/DataDog/dd-trace-py/issues/353 + if span.resource == SPAN_NAME: + # In the future, mask virtual path components in a + # URL e.g. /dispatch/abc123 becomes /dispatch/{{test_value}}/ + # Following investigation, this should be possible using + # [find_handler](https://docs.cherrypy.org/en/latest/_modules/cherrypy/_cpdispatch.html#Dispatcher.find_handler) + # but this may not be as easy as `cherrypy.request.dispatch.find_handler(cherrypy.request.path_info)` as + # this function only ever seems to return an empty list for the virtual path components. + + # For now, default resource is method and path: + # GET / + # POST /save + resource = "{} {}".format(cherrypy.request.method, cherrypy.request.path_info) + span.resource = compat.to_unicode(resource) + + url = compat.to_unicode(cherrypy.request.base + cherrypy.request.path_info) + status_code, _, _ = valid_status(cherrypy.response.status) + + trace_utils.set_http_meta( + span, + config.cherrypy, + method=cherrypy.request.method, + url=url, + status_code=status_code, + request_headers=cherrypy.request.headers, + response_headers=cherrypy.response.headers, + ) + + span.finish() + + # Clear our span just in case. + cherrypy.request._datadog_span = None + + +class TraceMiddleware(object): + def __init__(self, app, tracer, service="cherrypy", distributed_tracing=None): + self.app = app + + self.app.tools.tracer = TraceTool(app, tracer, service, distributed_tracing) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/consul/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/consul/__init__.py new file mode 100644 index 000000000..253c43503 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/consul/__init__.py @@ -0,0 +1,32 @@ +"""Instrument Consul to trace KV queries. + +Only supports tracing for the synchronous client. + +``patch_all`` will automatically patch your Consul client to make it work. +:: + + from ddtrace import Pin, patch + import consul + + # If not patched yet, you can patch consul specifically + patch(consul=True) + + # This will report a span with the default settings + client = consul.Consul(host="127.0.0.1", port=8500) + client.get("my-key") + + # Use a pin to specify metadata related to this client + Pin.override(client, service='consul-kv') +""" + +from ...utils.importlib import require_modules + + +required_modules = ["consul"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = ["patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/consul/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/consul/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..6c3137ec7 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/consul/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/consul/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/consul/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..6e7d33db7 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/consul/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/consul/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/consul/patch.py new file mode 100644 index 000000000..bb12fe86c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/consul/patch.py @@ -0,0 +1,61 @@ +import consul + +from ddtrace import config +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import consul as consulx +from ...pin import Pin +from ...utils import get_argument_value +from ...utils.wrappers import unwrap as _u + + +_KV_FUNCS = ["put", "get", "delete"] + + +def patch(): + if getattr(consul, "__datadog_patch", False): + return + setattr(consul, "__datadog_patch", True) + + pin = Pin(service=consulx.SERVICE, app=consulx.APP) + pin.onto(consul.Consul.KV) + + for f_name in _KV_FUNCS: + _w("consul", "Consul.KV.%s" % f_name, wrap_function(f_name)) + + +def unpatch(): + if not getattr(consul, "__datadog_patch", False): + return + setattr(consul, "__datadog_patch", False) + + for f_name in _KV_FUNCS: + _u(consul.Consul.KV, f_name) + + +def wrap_function(name): + def trace_func(wrapped, instance, args, kwargs): + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + # Only patch the synchronous implementation + if not isinstance(instance.agent.http, consul.std.HTTPClient): + return wrapped(*args, **kwargs) + + path = get_argument_value(args, kwargs, 0, "key") + resource = name.upper() + + with pin.tracer.trace(consulx.CMD, service=pin.service, resource=resource, span_type=SpanTypes.HTTP) as span: + span.set_tag(SPAN_MEASURED_KEY) + rate = config.consul.get_analytics_sample_rate() + if rate is not None: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, rate) + span.set_tag(consulx.KEY, path) + span.set_tag(consulx.CMD, resource) + return wrapped(*args, **kwargs) + + return trace_func diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dbapi/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dbapi/__init__.py new file mode 100644 index 000000000..12b9b6e48 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dbapi/__init__.py @@ -0,0 +1,271 @@ +""" +Generic dbapi tracing code. +""" +import six + +from ddtrace import config + +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import sql +from ...internal.logger import get_logger +from ...pin import Pin +from ...utils import ArgumentError +from ...utils import get_argument_value +from ...vendor import wrapt +from ..trace_utils import ext_service +from ..trace_utils import iswrapped + + +log = get_logger(__name__) + + +config._add( + "dbapi2", + dict( + _default_service="db", + trace_fetch_methods=None, # Part of the API. Should be implemented at the integration level. + ), +) + + +class TracedCursor(wrapt.ObjectProxy): + """TracedCursor wraps a psql cursor and traces its queries.""" + + def __init__(self, cursor, pin, cfg): + super(TracedCursor, self).__init__(cursor) + pin.onto(self) + name = pin.app or "sql" + self._self_datadog_name = "{}.query".format(name) + self._self_last_execute_operation = None + self._self_config = cfg or config.dbapi2 + + def _trace_method(self, method, name, resource, extra_tags, *args, **kwargs): + """ + Internal function to trace the call to the underlying cursor method + :param method: The callable to be wrapped + :param name: The name of the resulting span. + :param resource: The sql query. Sql queries are obfuscated on the agent side. + :param extra_tags: A dict of tags to store into the span's meta + :param args: The args that will be passed as positional args to the wrapped method + :param kwargs: The args that will be passed as kwargs to the wrapped method + :return: The result of the wrapped method invocation + """ + pin = Pin.get_from(self) + if not pin or not pin.enabled(): + return method(*args, **kwargs) + measured = name == self._self_datadog_name + + with pin.tracer.trace( + name, service=ext_service(pin, self._self_config), resource=resource, span_type=SpanTypes.SQL + ) as s: + if measured: + s.set_tag(SPAN_MEASURED_KEY) + # No reason to tag the query since it is set as the resource by the agent. See: + # https://github.com/DataDog/datadog-trace-agent/blob/bda1ebbf170dd8c5879be993bdd4dbae70d10fda/obfuscate/sql.go#L232 + s.set_tags(pin.tags) + s.set_tags(extra_tags) + + # set analytics sample rate if enabled but only for non-FetchTracedCursor + if not isinstance(self, FetchTracedCursor): + s.set_tag(ANALYTICS_SAMPLE_RATE_KEY, self._self_config.get_analytics_sample_rate()) + + try: + return method(*args, **kwargs) + finally: + row_count = self.__wrapped__.rowcount + s.set_metric("db.rowcount", row_count) + # Necessary for django integration backward compatibility. Django integration used to provide its own + # implementation of the TracedCursor, which used to store the row count into a tag instead of + # as a metric. Such custom implementation has been replaced by this generic dbapi implementation and + # this tag has been added since. + # Check row count is an integer type to avoid comparison type error + if isinstance(row_count, six.integer_types) and row_count >= 0: + s.set_tag(sql.ROWS, row_count) + + def executemany(self, query, *args, **kwargs): + """Wraps the cursor.executemany method""" + self._self_last_execute_operation = query + # Always return the result as-is + # DEV: Some libraries return `None`, others `int`, and others the cursor objects + # These differences should be overridden at the integration specific layer (e.g. in `sqlite3/patch.py`) + # FIXME[matt] properly handle kwargs here. arg names can be different + # with different libs. + return self._trace_method( + self.__wrapped__.executemany, + self._self_datadog_name, + query, + {"sql.executemany": "true"}, + query, + *args, + **kwargs + ) + + def execute(self, query, *args, **kwargs): + """Wraps the cursor.execute method""" + self._self_last_execute_operation = query + + # Always return the result as-is + # DEV: Some libraries return `None`, others `int`, and others the cursor objects + # These differences should be overridden at the integration specific layer (e.g. in `sqlite3/patch.py`) + return self._trace_method(self.__wrapped__.execute, self._self_datadog_name, query, {}, query, *args, **kwargs) + + def callproc(self, proc, *args): + """Wraps the cursor.callproc method""" + self._self_last_execute_operation = proc + return self._trace_method(self.__wrapped__.callproc, self._self_datadog_name, proc, {}, proc, *args) + + def __enter__(self): + # previous versions of the dbapi didn't support context managers. let's + # reference the func that would be called to ensure that errors + # messages will be the same. + self.__wrapped__.__enter__ + + # and finally, yield the traced cursor. + return self + + +class FetchTracedCursor(TracedCursor): + """ + Sub-class of :class:`TracedCursor` that also instruments `fetchone`, `fetchall`, and `fetchmany` methods. + + We do not trace these functions by default since they can get very noisy (e.g. `fetchone` with 100k rows). + """ + + def fetchone(self, *args, **kwargs): + """Wraps the cursor.fetchone method""" + span_name = "{}.{}".format(self._self_datadog_name, "fetchone") + return self._trace_method( + self.__wrapped__.fetchone, span_name, self._self_last_execute_operation, {}, *args, **kwargs + ) + + def fetchall(self, *args, **kwargs): + """Wraps the cursor.fetchall method""" + span_name = "{}.{}".format(self._self_datadog_name, "fetchall") + return self._trace_method( + self.__wrapped__.fetchall, span_name, self._self_last_execute_operation, {}, *args, **kwargs + ) + + def fetchmany(self, *args, **kwargs): + """Wraps the cursor.fetchmany method""" + span_name = "{}.{}".format(self._self_datadog_name, "fetchmany") + # We want to trace the information about how many rows were requested. Note that this number may be larger + # the number of rows actually returned if less then requested are available from the query. + size_tag_key = "db.fetch.size" + + try: + extra_tags = {size_tag_key: get_argument_value(args, kwargs, 0, "size")} + except ArgumentError: + default_array_size = getattr(self.__wrapped__, "arraysize", None) + extra_tags = {size_tag_key: default_array_size} if default_array_size else {} + + return self._trace_method( + self.__wrapped__.fetchmany, span_name, self._self_last_execute_operation, extra_tags, *args, **kwargs + ) + + +class TracedConnection(wrapt.ObjectProxy): + """TracedConnection wraps a Connection with tracing code.""" + + def __init__(self, conn, pin=None, cfg=None, cursor_cls=None): + if not cfg: + cfg = config.dbapi2 + # Set default cursor class if one was not provided + if not cursor_cls: + # Do not trace `fetch*` methods by default + cursor_cls = FetchTracedCursor if cfg.trace_fetch_methods else TracedCursor + + super(TracedConnection, self).__init__(conn) + name = _get_vendor(conn) + self._self_datadog_name = "{}.connection".format(name) + db_pin = pin or Pin(service=name, app=name) + db_pin.onto(self) + # wrapt requires prefix of `_self` for attributes that are only in the + # proxy (since some of our source objects will use `__slots__`) + self._self_cursor_cls = cursor_cls + self._self_config = cfg + + def __enter__(self): + """Context management is not defined by the dbapi spec. + + This means unfortunately that the database clients each define their own + implementations. + + The ones we know about are: + + - mysqlclient<2.0 which returns a cursor instance. >=2.0 returns a + connection instance. + - psycopg returns a connection. + - pyodbc returns a connection. + - pymysql doesn't implement it. + - sqlite3 returns the connection. + """ + r = self.__wrapped__.__enter__() + + if hasattr(r, "cursor"): + # r is Connection-like. + if r is self.__wrapped__: + # Return the reference to this proxy object. Returning r would + # return the untraced reference. + return self + else: + # r is a different connection object. + # This should not happen in practice but play it safe so that + # the original functionality is maintained. + return r + elif hasattr(r, "execute"): + # r is Cursor-like. + if iswrapped(r): + return r + else: + pin = Pin.get_from(self) + if not pin: + return r + return self._self_cursor_cls(r, pin, self._self_config) + else: + # Otherwise r is some other object, so maintain the functionality + # of the original. + return r + + def _trace_method(self, method, name, extra_tags, *args, **kwargs): + pin = Pin.get_from(self) + if not pin or not pin.enabled(): + return method(*args, **kwargs) + + with pin.tracer.trace(name, service=ext_service(pin, self._self_config)) as s: + s.set_tags(pin.tags) + s.set_tags(extra_tags) + + return method(*args, **kwargs) + + def cursor(self, *args, **kwargs): + cursor = self.__wrapped__.cursor(*args, **kwargs) + pin = Pin.get_from(self) + if not pin: + return cursor + return self._self_cursor_cls(cursor, pin, self._self_config) + + def commit(self, *args, **kwargs): + span_name = "{}.{}".format(self._self_datadog_name, "commit") + return self._trace_method(self.__wrapped__.commit, span_name, {}, *args, **kwargs) + + def rollback(self, *args, **kwargs): + span_name = "{}.{}".format(self._self_datadog_name, "rollback") + return self._trace_method(self.__wrapped__.rollback, span_name, {}, *args, **kwargs) + + +def _get_vendor(conn): + """Return the vendor (e.g postgres, mysql) of the given + database. + """ + try: + name = _get_module_name(conn) + except Exception: + log.debug("couldn't parse module name", exc_info=True) + name = "sql" + return sql.normalize_vendor(name) + + +def _get_module_name(conn): + return conn.__class__.__module__.split(".")[0] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dbapi/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dbapi/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..e6a5a475a Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dbapi/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__init__.py new file mode 100644 index 000000000..de596bf33 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__init__.py @@ -0,0 +1,160 @@ +""" +The Django__ integration traces requests, views, template renderers, database +and cache calls in a Django application. + + +Enable Django tracing automatically via ``ddtrace-run``:: + + ddtrace-run python manage.py runserver + + +Django tracing can also be enabled manually:: + + from ddtrace import patch_all + patch_all() + + +To have Django capture the tracer logs, ensure the ``LOGGING`` variable in +``settings.py`` looks similar to:: + + LOGGING = { + 'loggers': { + 'ddtrace': { + 'handlers': ['console'], + 'level': 'WARNING', + }, + }, + } + + +Configuration +~~~~~~~~~~~~~ +.. py:data:: ddtrace.config.django['distributed_tracing_enabled'] + + Whether or not to parse distributed tracing headers from requests received by your Django app. + + Default: ``True`` + +.. py:data:: ddtrace.config.django['service_name'] + + The service name reported for your Django app. + + Can also be configured via the ``DD_SERVICE`` environment variable. + + Default: ``'django'`` + +.. py:data:: ddtrace.config.django['cache_service_name'] + + The service name reported for your Django app cache layer. + + Can also be configured via the ``DD_DJANGO_CACHE_SERVICE_NAME`` environment variable. + + Default: ``'django'`` + +.. py:data:: ddtrace.config.django['database_service_name'] + + A string reported as the service name of the Django app database layer. + + Can also be configured via the ``DD_DJANGO_DATABASE_SERVICE_NAME`` environment variable. + + Takes precedence over database_service_name_prefix. + + Default: ``''`` + +.. py:data:: ddtrace.config.django['database_service_name_prefix'] + + A string to be prepended to the service name reported for your Django app database layer. + + Can also be configured via the ``DD_DJANGO_DATABASE_SERVICE_NAME_PREFIX`` environment variable. + + The database service name is the name of the database appended with 'db'. Has a lower precedence than database_service_name. + + Default: ``''`` + +.. py:data:: ddtrace.config.django["trace_fetch_methods"] + + Whether or not to trace fetch methods. + + Can also configured via the ``DD_DJANGO_TRACE_FETCH_METHODS`` environment variable. + + Default: ``False`` + +.. py:data:: ddtrace.config.django['instrument_middleware'] + + Whether or not to instrument middleware. + + Can also be enabled with the ``DD_DJANGO_INSTRUMENT_MIDDLEWARE`` environment variable. + + Default: ``True`` + +.. py:data:: ddtrace.config.django['instrument_databases'] + + Whether or not to instrument databases. + + Default: ``True`` + +.. py:data:: ddtrace.config.django['instrument_caches'] + + Whether or not to instrument caches. + + Default: ``True`` + +.. py:data:: ddtrace.config.django['trace_query_string'] + + Whether or not to include the query string as a tag. + + Default: ``False`` + +.. py:data:: ddtrace.config.django['include_user_name'] + + Whether or not to include the authenticated user's username as a tag on the root request span. + + Default: ``True`` + +.. py:data:: ddtrace.config.django['use_handler_resource_format'] + + Whether or not to use the resource format `"{method} {handler}"`. Can also be + enabled with the ``DD_DJANGO_USE_HANDLER_RESOURCE_FORMAT`` environment + variable. + + The default resource format for Django >= 2.2.0 is otherwise `"{method} {urlpattern}"`. + + Default: ``False`` + +.. py:data:: ddtrace.config.django['use_legacy_resource_format'] + + Whether or not to use the legacy resource format `"{handler}"`. Can also be + enabled with the ``DD_DJANGO_USE_LEGACY_RESOURCE_FORMAT`` environment + variable. + + The default resource format for Django >= 2.2.0 is otherwise `"{method} {urlpattern}"`. + + Default: ``False`` + +Example:: + + from ddtrace import config + + # Enable distributed tracing + config.django['distributed_tracing_enabled'] = True + + # Override service name + config.django['service_name'] = 'custom-service-name' + + +:ref:`Headers tracing ` is supported for this integration. + +.. __: https://www.djangoproject.com/ +""" # noqa: E501 +from ...utils.importlib import require_modules + + +required_modules = ["django"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from . import patch as _patch + from .patch import patch + from .patch import unpatch + + __all__ = ["patch", "unpatch", "_patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..5cb3c7339 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/_asgi.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/_asgi.cpython-38.pyc new file mode 100644 index 000000000..84ed37d4b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/_asgi.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/compat.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/compat.cpython-38.pyc new file mode 100644 index 000000000..92702d12f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/compat.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..ea069b9e8 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/restframework.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/restframework.cpython-38.pyc new file mode 100644 index 000000000..6f9ede4ca Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/restframework.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/utils.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/utils.cpython-38.pyc new file mode 100644 index 000000000..04a6ef723 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/__pycache__/utils.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/_asgi.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/_asgi.py new file mode 100644 index 000000000..17d247d44 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/_asgi.py @@ -0,0 +1,36 @@ +""" +Module providing async hooks. Do not import this module unless using Python >= 3.6. +""" +from ddtrace.contrib.asgi import span_from_scope + +from .. import trace_utils +from ...utils import get_argument_value +from .utils import REQUEST_DEFAULT_RESOURCE +from .utils import _after_request_tags +from .utils import _before_request_tags + + +@trace_utils.with_traced_module +async def traced_get_response_async(django, pin, func, instance, args, kwargs): + """Trace django.core.handlers.base.BaseHandler.get_response() (or other implementations). + + This is the main entry point for requests. + + Django requests are handled by a Handler.get_response method (inherited from base.BaseHandler). + This method invokes the middleware chain and returns the response generated by the chain. + """ + request = get_argument_value(args, kwargs, 0, "request") + span = span_from_scope(request.scope) + if span is None: + return await func(*args, **kwargs) + + # Reset the span resource so we can know if it was modified during the request or not + span.resource = REQUEST_DEFAULT_RESOURCE + _before_request_tags(pin, span, request) + response = None + try: + response = await func(*args, **kwargs) + finally: + # DEV: Always set these tags, this is where `span.resource` is set + _after_request_tags(pin, span, request, response) + return response diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/compat.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/compat.py new file mode 100644 index 000000000..8c53e7aeb --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/compat.py @@ -0,0 +1,32 @@ +import django + + +if django.VERSION >= (1, 10, 1): + from django.urls import get_resolver + + def user_is_authenticated(user): + # Explicit comparison due to the following bug + # https://code.djangoproject.com/ticket/26988 + return user.is_authenticated == True # noqa E712 + + +else: + from django.conf import settings + from django.core import urlresolvers + + def user_is_authenticated(user): + return user.is_authenticated() + + if django.VERSION >= (1, 9, 0): + + def get_resolver(urlconf=None): + urlconf = urlconf or settings.ROOT_URLCONF + urlresolvers.set_urlconf(urlconf) + return urlresolvers.get_resolver(urlconf) + + else: + + def get_resolver(urlconf=None): + urlconf = urlconf or settings.ROOT_URLCONF + urlresolvers.set_urlconf(urlconf) + return urlresolvers.RegexURLResolver(r"^/", urlconf) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/patch.py new file mode 100644 index 000000000..6d27e8253 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/patch.py @@ -0,0 +1,541 @@ +""" +The Django patching works as follows: + +Django internals are instrumented via normal `patch()`. + +`django.apps.registry.Apps.populate` is patched to add instrumentation for any +specific Django apps like Django Rest Framework (DRF). +""" +from inspect import getmro +from inspect import isclass +from inspect import isfunction +import sys + +from ddtrace import Pin +from ddtrace import config +from ddtrace.constants import SPAN_MEASURED_KEY +from ddtrace.contrib import dbapi +from ddtrace.contrib import func_name + +from ...utils import get_argument_value + + +try: + from psycopg2._psycopg import cursor as psycopg_cursor_cls + + from ddtrace.contrib.psycopg.patch import Psycopg2TracedCursor +except ImportError: + psycopg_cursor_cls = None + Psycopg2TracedCursor = None + +from ddtrace.ext import SpanTypes +from ddtrace.ext import http +from ddtrace.ext import sql as sqlx +from ddtrace.internal.compat import maybe_stringify +from ddtrace.internal.logger import get_logger +from ddtrace.utils.formats import asbool +from ddtrace.utils.formats import get_env +from ddtrace.vendor import wrapt + +from . import utils +from .. import trace_utils + + +log = get_logger(__name__) + +config._add( + "django", + dict( + _default_service="django", + cache_service_name=get_env("django", "cache_service_name") or "django", + database_service_name_prefix=get_env("django", "database_service_name_prefix", default=""), + database_service_name=get_env("django", "database_service_name", default=""), + trace_fetch_methods=asbool(get_env("django", "trace_fetch_methods", default=False)), + distributed_tracing_enabled=True, + instrument_middleware=asbool(get_env("django", "instrument_middleware", default=True)), + instrument_databases=True, + instrument_caches=True, + analytics_enabled=None, # None allows the value to be overridden by the global config + analytics_sample_rate=None, + trace_query_string=None, # Default to global config + include_user_name=True, + use_handler_resource_format=asbool(get_env("django", "use_handler_resource_format", default=False)), + use_legacy_resource_format=asbool(get_env("django", "use_legacy_resource_format", default=False)), + ), +) + + +def patch_conn(django, conn): + def cursor(django, pin, func, instance, args, kwargs): + alias = getattr(conn, "alias", "default") + + if config.django.database_service_name: + service = config.django.database_service_name + else: + database_prefix = config.django.database_service_name_prefix + service = "{}{}{}".format(database_prefix, alias, "db") + + vendor = getattr(conn, "vendor", "db") + prefix = sqlx.normalize_vendor(vendor) + tags = { + "django.db.vendor": vendor, + "django.db.alias": alias, + } + pin = Pin(service, tags=tags, tracer=pin.tracer, app=prefix) + cursor = func(*args, **kwargs) + traced_cursor_cls = dbapi.TracedCursor + if ( + Psycopg2TracedCursor is not None + and hasattr(cursor, "cursor") + and isinstance(cursor.cursor, psycopg_cursor_cls) + ): + traced_cursor_cls = Psycopg2TracedCursor + return traced_cursor_cls(cursor, pin, config.django) + + if not isinstance(conn.cursor, wrapt.ObjectProxy): + conn.cursor = wrapt.FunctionWrapper(conn.cursor, trace_utils.with_traced_module(cursor)(django)) + + +def instrument_dbs(django): + def get_connection(wrapped, instance, args, kwargs): + conn = wrapped(*args, **kwargs) + try: + patch_conn(django, conn) + except Exception: + log.debug("Error instrumenting database connection %r", conn, exc_info=True) + return conn + + if not isinstance(django.db.utils.ConnectionHandler.__getitem__, wrapt.ObjectProxy): + django.db.utils.ConnectionHandler.__getitem__ = wrapt.FunctionWrapper( + django.db.utils.ConnectionHandler.__getitem__, get_connection + ) + + +@trace_utils.with_traced_module +def traced_cache(django, pin, func, instance, args, kwargs): + if not config.django.instrument_caches: + return func(*args, **kwargs) + + # get the original function method + with pin.tracer.trace("django.cache", span_type=SpanTypes.CACHE, service=config.django.cache_service_name) as span: + # update the resource name and tag the cache backend + span.resource = utils.resource_from_cache_prefix(func_name(func), instance) + cache_backend = "{}.{}".format(instance.__module__, instance.__class__.__name__) + span._set_str_tag("django.cache.backend", cache_backend) + + if args: + keys = utils.quantize_key_values(args[0]) + span._set_str_tag("django.cache.key", str(keys)) + + return func(*args, **kwargs) + + +def instrument_caches(django): + cache_backends = set([cache["BACKEND"] for cache in django.conf.settings.CACHES.values()]) + for cache_path in cache_backends: + split = cache_path.split(".") + cache_module = ".".join(split[:-1]) + cache_cls = split[-1] + for method in ["get", "set", "add", "delete", "incr", "decr", "get_many", "set_many", "delete_many"]: + try: + cls = django.utils.module_loading.import_string(cache_path) + # DEV: this can be removed when we add an idempotent `wrap` + if not trace_utils.iswrapped(cls, method): + trace_utils.wrap(cache_module, "{0}.{1}".format(cache_cls, method), traced_cache(django)) + except Exception: + log.debug("Error instrumenting cache %r", cache_path, exc_info=True) + + +@trace_utils.with_traced_module +def traced_populate(django, pin, func, instance, args, kwargs): + """django.apps.registry.Apps.populate is the method used to populate all the apps. + + It is used as a hook to install instrumentation for 3rd party apps (like DRF). + + `populate()` works in 3 phases: + + - Phase 1: Initializes the app configs and imports the app modules. + - Phase 2: Imports models modules for each app. + - Phase 3: runs ready() of each app config. + + If all 3 phases successfully run then `instance.ready` will be `True`. + """ + + # populate() can be called multiple times, we don't want to instrument more than once + if instance.ready: + log.debug("Django instrumentation already installed, skipping.") + return func(*args, **kwargs) + + ret = func(*args, **kwargs) + + if not instance.ready: + log.debug("populate() failed skipping instrumentation.") + return ret + + settings = django.conf.settings + + # Instrument databases + if config.django.instrument_databases: + try: + instrument_dbs(django) + except Exception: + log.debug("Error instrumenting Django database connections", exc_info=True) + + # Instrument caches + if config.django.instrument_caches: + try: + instrument_caches(django) + except Exception: + log.debug("Error instrumenting Django caches", exc_info=True) + + # Instrument Django Rest Framework if it's installed + INSTALLED_APPS = getattr(settings, "INSTALLED_APPS", []) + + if "rest_framework" in INSTALLED_APPS: + try: + from .restframework import patch_restframework + + patch_restframework(django) + except Exception: + log.debug("Error patching rest_framework", exc_info=True) + + return ret + + +def traced_func(django, name, resource=None, ignored_excs=None): + """Returns a function to trace Django functions.""" + + def wrapped(django, pin, func, instance, args, kwargs): + with pin.tracer.trace(name, resource=resource) as s: + if ignored_excs: + for exc in ignored_excs: + s._ignore_exception(exc) + return func(*args, **kwargs) + + return trace_utils.with_traced_module(wrapped)(django) + + +def traced_process_exception(django, name, resource=None): + def wrapped(django, pin, func, instance, args, kwargs): + with pin.tracer.trace(name, resource=resource) as span: + resp = func(*args, **kwargs) + + # If the response code is erroneous then grab the traceback + # and set an error. + if hasattr(resp, "status_code") and 500 <= resp.status_code < 600: + span.set_traceback() + return resp + + return trace_utils.with_traced_module(wrapped)(django) + + +@trace_utils.with_traced_module +def traced_load_middleware(django, pin, func, instance, args, kwargs): + """Patches django.core.handlers.base.BaseHandler.load_middleware to instrument all middlewares.""" + settings_middleware = [] + # Gather all the middleware + if getattr(django.conf.settings, "MIDDLEWARE", None): + settings_middleware += django.conf.settings.MIDDLEWARE + if getattr(django.conf.settings, "MIDDLEWARE_CLASSES", None): + settings_middleware += django.conf.settings.MIDDLEWARE_CLASSES + + # Iterate over each middleware provided in settings.py + # Each middleware can either be a function or a class + for mw_path in settings_middleware: + mw = django.utils.module_loading.import_string(mw_path) + + # Instrument function-based middleware + if isfunction(mw) and not trace_utils.iswrapped(mw): + split = mw_path.split(".") + if len(split) < 2: + continue + base = ".".join(split[:-1]) + attr = split[-1] + + # DEV: We need to have a closure over `mw_path` for the resource name or else + # all function based middleware will share the same resource name + def _wrapper(resource): + # Function-based middleware is a factory which returns a handler function for requests. + # So instead of tracing the factory, we want to trace its returned value. + # We wrap the factory to return a traced version of the handler function. + def wrapped_factory(func, instance, args, kwargs): + # r is the middleware handler function returned from the factory + r = func(*args, **kwargs) + if r: + return wrapt.FunctionWrapper( + r, + traced_func(django, "django.middleware", resource=resource), + ) + # If r is an empty middleware function (i.e. returns None), don't wrap since + # NoneType cannot be called + else: + return r + + return wrapped_factory + + trace_utils.wrap(base, attr, _wrapper(resource=mw_path)) + + # Instrument class-based middleware + elif isclass(mw): + for hook in [ + "process_request", + "process_response", + "process_view", + "process_template_response", + "__call__", + ]: + if hasattr(mw, hook) and not trace_utils.iswrapped(mw, hook): + trace_utils.wrap( + mw, hook, traced_func(django, "django.middleware", resource=mw_path + ".{0}".format(hook)) + ) + # Do a little extra for `process_exception` + if hasattr(mw, "process_exception") and not trace_utils.iswrapped(mw, "process_exception"): + res = mw_path + ".{0}".format("process_exception") + trace_utils.wrap( + mw, "process_exception", traced_process_exception(django, "django.middleware", resource=res) + ) + + return func(*args, **kwargs) + + +@trace_utils.with_traced_module +def traced_get_response(django, pin, func, instance, args, kwargs): + """Trace django.core.handlers.base.BaseHandler.get_response() (or other implementations). + + This is the main entry point for requests. + + Django requests are handled by a Handler.get_response method (inherited from base.BaseHandler). + This method invokes the middleware chain and returns the response generated by the chain. + """ + + request = get_argument_value(args, kwargs, 0, "request") + if request is None: + return func(*args, **kwargs) + + trace_utils.activate_distributed_headers(pin.tracer, int_config=config.django, request_headers=request.META) + + with pin.tracer.trace( + "django.request", + resource=utils.REQUEST_DEFAULT_RESOURCE, + service=trace_utils.int_service(pin, config.django), + span_type=SpanTypes.WEB, + ) as span: + utils._before_request_tags(pin, span, request) + span.metrics[SPAN_MEASURED_KEY] = 1 + + response = None + try: + response = func(*args, **kwargs) + return response + finally: + # DEV: Always set these tags, this is where `span.resource` is set + utils._after_request_tags(pin, span, request, response) + + +@trace_utils.with_traced_module +def traced_template_render(django, pin, wrapped, instance, args, kwargs): + """Instrument django.template.base.Template.render for tracing template rendering.""" + template_name = maybe_stringify(getattr(instance, "name", None)) + if template_name: + resource = template_name + else: + resource = "{0}.{1}".format(func_name(instance), wrapped.__name__) + + with pin.tracer.trace("django.template.render", resource=resource, span_type=http.TEMPLATE) as span: + if template_name: + span._set_str_tag("django.template.name", template_name) + engine = getattr(instance, "engine", None) + if engine: + span._set_str_tag("django.template.engine.class", func_name(engine)) + + return wrapped(*args, **kwargs) + + +def instrument_view(django, view): + """ + Helper to wrap Django views. + + We want to wrap all lifecycle/http method functions for every class in the MRO for this view + """ + if hasattr(view, "__mro__"): + for cls in reversed(getmro(view)): + _instrument_view(django, cls) + + return _instrument_view(django, view) + + +def _instrument_view(django, view): + """Helper to wrap Django views.""" + # All views should be callable, double check before doing anything + if not callable(view): + return view + + # Patch view HTTP methods and lifecycle methods + http_method_names = getattr(view, "http_method_names", ("get", "delete", "post", "options", "head")) + lifecycle_methods = ("setup", "dispatch", "http_method_not_allowed") + for name in list(http_method_names) + list(lifecycle_methods): + try: + func = getattr(view, name, None) + if not func or isinstance(func, wrapt.ObjectProxy): + continue + + resource = "{0}.{1}".format(func_name(view), name) + op_name = "django.view.{0}".format(name) + trace_utils.wrap(view, name, traced_func(django, name=op_name, resource=resource)) + except Exception: + log.debug("Failed to instrument Django view %r function %s", view, name, exc_info=True) + + # Patch response methods + response_cls = getattr(view, "response_class", None) + if response_cls: + methods = ("render",) + for name in methods: + try: + func = getattr(response_cls, name, None) + # Do not wrap if the method does not exist or is already wrapped + if not func or isinstance(func, wrapt.ObjectProxy): + continue + + resource = "{0}.{1}".format(func_name(response_cls), name) + op_name = "django.response.{0}".format(name) + trace_utils.wrap(response_cls, name, traced_func(django, name=op_name, resource=resource)) + except Exception: + log.debug("Failed to instrument Django response %r function %s", response_cls, name, exc_info=True) + + # If the view itself is not wrapped, wrap it + if not isinstance(view, wrapt.ObjectProxy): + view = wrapt.FunctionWrapper( + view, traced_func(django, "django.view", resource=func_name(view), ignored_excs=[django.http.Http404]) + ) + return view + + +@trace_utils.with_traced_module +def traced_urls_path(django, pin, wrapped, instance, args, kwargs): + """Wrapper for url path helpers to ensure all views registered as urls are traced.""" + try: + if "view" in kwargs: + kwargs["view"] = instrument_view(django, kwargs["view"]) + elif len(args) >= 2: + args = list(args) + args[1] = instrument_view(django, args[1]) + args = tuple(args) + except Exception: + log.debug("Failed to instrument Django url path %r %r", args, kwargs, exc_info=True) + return wrapped(*args, **kwargs) + + +@trace_utils.with_traced_module +def traced_as_view(django, pin, func, instance, args, kwargs): + """ + Wrapper for django's View.as_view class method + """ + try: + instrument_view(django, instance) + except Exception: + log.debug("Failed to instrument Django view %r", instance, exc_info=True) + view = func(*args, **kwargs) + return wrapt.FunctionWrapper(view, traced_func(django, "django.view", resource=func_name(view))) + + +@trace_utils.with_traced_module +def traced_get_asgi_application(django, pin, func, instance, args, kwargs): + from ddtrace.contrib.asgi import TraceMiddleware + + def django_asgi_modifier(span, scope): + span.name = "django.request" + + return TraceMiddleware(func(*args, **kwargs), integration_config=config.django, span_modifier=django_asgi_modifier) + + +def _patch(django): + Pin().onto(django) + trace_utils.wrap(django, "apps.registry.Apps.populate", traced_populate(django)) + + # DEV: this check will be replaced with import hooks in the future + if "django.core.handlers.base" not in sys.modules: + import django.core.handlers.base + + if config.django.instrument_middleware: + trace_utils.wrap(django, "core.handlers.base.BaseHandler.load_middleware", traced_load_middleware(django)) + + trace_utils.wrap(django, "core.handlers.base.BaseHandler.get_response", traced_get_response(django)) + if hasattr(django.core.handlers.base.BaseHandler, "get_response_async"): + # Have to inline this import as the module contains syntax incompatible with Python 3.5 and below + from ._asgi import traced_get_response_async + + trace_utils.wrap(django, "core.handlers.base.BaseHandler.get_response_async", traced_get_response_async(django)) + + # Only wrap get_asgi_application if get_response_async exists. Otherwise we will effectively double-patch + # because get_response and get_asgi_application will be used. + if "django.core.asgi" not in sys.modules: + try: + import django.core.asgi + except ImportError: + pass + else: + trace_utils.wrap(django, "core.asgi.get_asgi_application", traced_get_asgi_application(django)) + + # DEV: this check will be replaced with import hooks in the future + if "django.template.base" not in sys.modules: + import django.template.base + trace_utils.wrap(django, "template.base.Template.render", traced_template_render(django)) + + # DEV: this check will be replaced with import hooks in the future + if "django.conf.urls.static" not in sys.modules: + import django.conf.urls.static + trace_utils.wrap(django, "conf.urls.url", traced_urls_path(django)) + if django.VERSION >= (2, 0, 0): + trace_utils.wrap(django, "urls.path", traced_urls_path(django)) + trace_utils.wrap(django, "urls.re_path", traced_urls_path(django)) + + # DEV: this check will be replaced with import hooks in the future + if "django.views.generic.base" not in sys.modules: + import django.views.generic.base + trace_utils.wrap(django, "views.generic.base.View.as_view", traced_as_view(django)) + + +def patch(): + # DEV: this import will eventually be replaced with the module given from an import hook + import django + + if django.VERSION < (1, 10, 0): + utils.Resolver404 = django.core.urlresolvers.Resolver404 + else: + utils.Resolver404 = django.urls.exceptions.Resolver404 + + utils.DJANGO22 = django.VERSION >= (2, 2, 0) + + if getattr(django, "_datadog_patch", False): + return + _patch(django) + + setattr(django, "_datadog_patch", True) + + +def _unpatch(django): + trace_utils.unwrap(django.apps.registry.Apps, "populate") + trace_utils.unwrap(django.core.handlers.base.BaseHandler, "load_middleware") + trace_utils.unwrap(django.core.handlers.base.BaseHandler, "get_response") + trace_utils.unwrap(django.core.handlers.base.BaseHandler, "get_response_async") + trace_utils.unwrap(django.template.base.Template, "render") + trace_utils.unwrap(django.conf.urls.static, "static") + trace_utils.unwrap(django.conf.urls, "url") + if django.VERSION >= (2, 0, 0): + trace_utils.unwrap(django.urls, "path") + trace_utils.unwrap(django.urls, "re_path") + trace_utils.unwrap(django.views.generic.base.View, "as_view") + for conn in django.db.connections.all(): + trace_utils.unwrap(conn, "cursor") + trace_utils.unwrap(django.db.utils.ConnectionHandler, "__getitem__") + + +def unpatch(): + import django + + if not getattr(django, "_datadog_patch", False): + return + + _unpatch(django) + + setattr(django, "_datadog_patch", False) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/restframework.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/restframework.py new file mode 100644 index 000000000..6bab20ca2 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/restframework.py @@ -0,0 +1,33 @@ +import rest_framework.views + +from ddtrace.vendor.wrapt import wrap_function_wrapper as wrap + +from ...utils.wrappers import iswrapped +from ..trace_utils import with_traced_module + + +@with_traced_module +def _traced_handle_exception(django, pin, wrapped, instance, args, kwargs): + """Sets the error message, error type and exception stack trace to the current span + before calling the original exception handler. + """ + span = pin.tracer.current_span() + + if span is not None: + span.set_traceback() + + return wrapped(*args, **kwargs) + + +def patch_restframework(django): + """Patches rest_framework app. + + To trace exceptions occurring during view processing we currently use a TraceExceptionMiddleware. + However the rest_framework handles exceptions before they come to our middleware. + So we need to manually patch the rest_framework exception handler + to set the exception stack trace in the current span. + """ + + # trace the handle_exception method + if not iswrapped(rest_framework.views.APIView, "handle_exception"): + wrap("rest_framework.views", "APIView.handle_exception", _traced_handle_exception(django)) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/utils.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/utils.py new file mode 100644 index 000000000..f39f63d28 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/django/utils.py @@ -0,0 +1,296 @@ +from django.utils.functional import SimpleLazyObject +import six + +from ddtrace import config +from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY +from ddtrace.constants import SPAN_MEASURED_KEY +from ddtrace.contrib import func_name +from ddtrace.ext import SpanTypes +from ddtrace.propagation.utils import from_wsgi_header + +from .. import trace_utils +from ...internal.logger import get_logger +from .compat import get_resolver +from .compat import user_is_authenticated + + +log = get_logger(__name__) + + +# Set on patch, when django is imported +Resolver404 = None +DJANGO22 = None + +REQUEST_DEFAULT_RESOURCE = "__django_request" + + +def resource_from_cache_prefix(resource, cache): + """ + Combine the resource name with the cache prefix (if any) + """ + if getattr(cache, "key_prefix", None): + name = " ".join((resource, cache.key_prefix)) + else: + name = resource + + # enforce lowercase to make the output nicer to read + return name.lower() + + +def quantize_key_values(key): + """ + Used in the Django trace operation method, it ensures that if a dict + with values is used, we removes the values from the span meta + attributes. For example:: + + >>> quantize_key_values({'key': 'value'}) + # returns ['key'] + """ + if isinstance(key, dict): + return key.keys() + + return key + + +def get_django_2_route(request, resolver_match): + # Try to use `resolver_match.route` if available + # Otherwise, look for `resolver.pattern.regex.pattern` + route = resolver_match.route + if route: + return route + + resolver = get_resolver(getattr(request, "urlconf", None)) + if resolver: + try: + return resolver.pattern.regex.pattern + except AttributeError: + pass + + return None + + +def set_tag_array(span, prefix, value): + """Helper to set a span tag as a single value or an array""" + if not value: + return + + if len(value) == 1: + if value[0]: + span._set_str_tag(prefix, value[0]) + else: + for i, v in enumerate(value, start=0): + if v: + span._set_str_tag("".join((prefix, ".", str(i))), v) + + +def get_request_uri(request): + """ + Helper to rebuild the original request url + + query string or fragments are not included. + """ + # DEV: Use django.http.request.HttpRequest._get_raw_host() when available + # otherwise back-off to PEP 333 as done in django 1.8.x + if hasattr(request, "_get_raw_host"): + host = request._get_raw_host() + else: + try: + # Try to build host how Django would have + # https://github.com/django/django/blob/e8d0d2a5efc8012dcc8bf1809dec065ebde64c81/django/http/request.py#L85-L102 + if "HTTP_HOST" in request.META: + host = request.META["HTTP_HOST"] + else: + host = request.META["SERVER_NAME"] + port = str(request.META["SERVER_PORT"]) + if port != ("443" if request.is_secure() else "80"): + host = "".join((host, ":", port)) + except Exception: + # This really shouldn't ever happen, but lets guard here just in case + log.debug("Failed to build Django request host", exc_info=True) + host = "unknown" + + # If request scheme is missing, possible in case where wsgi.url_scheme + # environ has not been set, return None and skip providing a uri + if request.scheme is None: + return + + # Build request url from the information available + # DEV: We are explicitly omitting query strings since they may contain sensitive information + urlparts = {"scheme": request.scheme, "netloc": host, "path": request.path} + + # If any url part is a SimpleLazyObject, use its __class__ property to cast + # str/bytes and allow for _setup() to execute + for (k, v) in urlparts.items(): + if isinstance(v, SimpleLazyObject): + if issubclass(v.__class__, str): + v = str(v) + elif issubclass(v.__class__, bytes): + v = bytes(v) + else: + # lazy object that is not str or bytes should not happen here + # but if it does skip providing a uri + log.debug( + "Skipped building Django request uri, %s is SimpleLazyObject wrapping a %s class", + k, + v.__class__.__name__, + ) + return None + urlparts[k] = six.ensure_text(v) + + return "".join((urlparts["scheme"], "://", urlparts["netloc"], urlparts["path"])) + + +def _set_resolver_tags(pin, span, request): + # Default to just the HTTP method when we cannot determine a reasonable resource + resource = request.method + + try: + # Get resolver match result and build resource name pieces + resolver_match = request.resolver_match + if not resolver_match: + # The request quite likely failed (e.g. 404) so we do the resolution anyway. + resolver = get_resolver(getattr(request, "urlconf", None)) + resolver_match = resolver.resolve(request.path_info) + handler = func_name(resolver_match[0]) + + if config.django.use_handler_resource_format: + resource = " ".join((request.method, handler)) + elif config.django.use_legacy_resource_format: + resource = handler + else: + # In Django >= 2.2.0 we can access the original route or regex pattern + # TODO: Validate if `resolver.pattern.regex.pattern` is available on django<2.2 + if DJANGO22: + # Determine the resolver and resource name for this request + route = get_django_2_route(request, resolver_match) + if route: + resource = " ".join((request.method, route)) + span._set_str_tag("http.route", route) + else: + resource = " ".join((request.method, handler)) + + span._set_str_tag("django.view", resolver_match.view_name) + set_tag_array(span, "django.namespace", resolver_match.namespaces) + + # Django >= 2.0.0 + if hasattr(resolver_match, "app_names"): + set_tag_array(span, "django.app", resolver_match.app_names) + + except Resolver404: + # Normalize all 404 requests into a single resource name + # DEV: This is for potential cardinality issues + resource = " ".join((request.method, "404")) + except Exception: + log.debug( + "Failed to resolve request path %r with path info %r", + request, + getattr(request, "path_info", "not-set"), + exc_info=True, + ) + finally: + # Only update the resource name if it was not explicitly set + # by anyone during the request lifetime + if span.resource == REQUEST_DEFAULT_RESOURCE: + span.resource = resource + + +def _before_request_tags(pin, span, request): + # DEV: Do not set `span.resource` here, leave it as `None` + # until `_set_resolver_tags` so we can know if the user + # has explicitly set it during the request lifetime + span.service = trace_utils.int_service(pin, config.django) + span.span_type = SpanTypes.WEB + span.metrics[SPAN_MEASURED_KEY] = 1 + + analytics_sr = config.django.get_analytics_sample_rate(use_global_config=True) + if analytics_sr is not None: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, analytics_sr) + + span._set_str_tag("django.request.class", func_name(request)) + + +def _after_request_tags(pin, span, request, response): + # Response can be None in the event that the request failed + # We still want to set additional request tags that are resolved + # during the request. + + try: + user = getattr(request, "user", None) + if user is not None: + # Note: getattr calls to user / user_is_authenticated may result in ImproperlyConfigured exceptions from + # Django's get_user_model(): + # https://github.com/django/django/blob/a464ead29db8bf6a27a5291cad9eb3f0f3f0472b/django/contrib/auth/__init__.py + try: + if hasattr(user, "is_authenticated"): + span._set_str_tag("django.user.is_authenticated", str(user_is_authenticated(user))) + + uid = getattr(user, "pk", None) + if uid: + span._set_str_tag("django.user.id", str(uid)) + + if config.django.include_user_name: + username = getattr(user, "username", None) + if username: + span._set_str_tag("django.user.name", username) + except Exception: + log.debug("Error retrieving authentication information for user %r", user, exc_info=True) + + if response: + status = response.status_code + span._set_str_tag("django.response.class", func_name(response)) + if hasattr(response, "template_name"): + # template_name is a bit of a misnomer, as it could be any of: + # a list of strings, a tuple of strings, a single string, or an instance of Template + # for more detail, see: + # https://docs.djangoproject.com/en/3.0/ref/template-response/#django.template.response.SimpleTemplateResponse.template_name + template = response.template_name + + if isinstance(template, six.string_types): + template_names = [template] + elif isinstance( + template, + ( + list, + tuple, + ), + ): + template_names = template + elif hasattr(template, "template"): + # ^ checking by attribute here because + # django backend implementations don't have a common base + # `.template` is also the most consistent across django versions + template_names = [template.template.name] + else: + template_names = None + + set_tag_array(span, "django.response.template", template_names) + + url = get_request_uri(request) + + if DJANGO22: + request_headers = request.headers + else: + request_headers = {} + for header, value in request.META.items(): + name = from_wsgi_header(header) + if name: + request_headers[name] = value + + # DEV: Resolve the view and resource name at the end of the request in case + # urlconf changes at any point during the request + _set_resolver_tags(pin, span, request) + + response_headers = dict(response.items()) if response else {} + trace_utils.set_http_meta( + span, + config.django, + method=request.method, + url=url, + status_code=status, + query=request.META.get("QUERY_STRING", None), + request_headers=request_headers, + response_headers=response_headers, + ) + finally: + if span.resource == REQUEST_DEFAULT_RESOURCE: + span.resource = request.method diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/__init__.py new file mode 100644 index 000000000..d45bb6162 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/__init__.py @@ -0,0 +1,49 @@ +""" +Instrument dogpile.cache__ to report all cached lookups. + +This will add spans around the calls to your cache backend (e.g. redis, memory, +etc). The spans will also include the following tags: + +- key/keys: The key(s) dogpile passed to your backend. Note that this will be + the output of the region's ``function_key_generator``, but before any key + mangling is applied (i.e. the region's ``key_mangler``). +- region: Name of the region. +- backend: Name of the backend class. +- hit: If the key was found in the cache. +- expired: If the key is expired. This is only relevant if the key was found. + +While cache tracing will generally already have keys in tags, some caching +setups will not have useful tag values - such as when you're using consistent +hashing with memcached - the key(s) will appear as a mangled hash. +:: + + # Patch before importing dogpile.cache + from ddtrace import patch + patch(dogpile_cache=True) + + from dogpile.cache import make_region + + region = make_region().configure( + "dogpile.cache.pylibmc", + expiration_time=3600, + arguments={"url": ["127.0.0.1"]}, + ) + + @region.cache_on_arguments() + def hello(name): + # Some complicated, slow calculation + return "Hello, {}".format(name) + +.. __: https://dogpilecache.sqlalchemy.org/ +""" +from ...utils.importlib import require_modules + + +required_modules = ["dogpile.cache"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = ["patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..fe83edb94 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/__pycache__/lock.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/__pycache__/lock.cpython-38.pyc new file mode 100644 index 000000000..f8dbb9b38 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/__pycache__/lock.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..0ba7c71d6 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/__pycache__/region.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/__pycache__/region.cpython-38.pyc new file mode 100644 index 000000000..5d51c3524 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/__pycache__/region.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/lock.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/lock.py new file mode 100644 index 000000000..5e293c052 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/lock.py @@ -0,0 +1,39 @@ +import dogpile + +from ...pin import Pin +from ...utils.formats import asbool + + +def _wrap_lock_ctor(func, instance, args, kwargs): + """ + This seems rather odd. But to track hits, we need to patch the wrapped function that + dogpile passes to the region and locks. Unfortunately it's a closure defined inside + the get_or_create* methods themselves, so we can't easily patch those. + """ + func(*args, **kwargs) + ori_backend_fetcher = instance.value_and_created_fn + + def wrapped_backend_fetcher(): + pin = Pin.get_from(dogpile.cache) + if not pin or not pin.enabled(): + return ori_backend_fetcher() + + hit = False + expired = True + try: + value, createdtime = ori_backend_fetcher() + hit = value is not dogpile.cache.api.NO_VALUE + # dogpile sometimes returns None, but only checks for truthiness. Coalesce + # to minimize APM users' confusion. + expired = instance._is_expired(createdtime) or False + return value, createdtime + finally: + # Keys are checked in random order so the 'final' answer for partial hits + # should really be false (ie. if any are 'negative', then the tag value + # should be). This means ANDing all hit values and ORing all expired values. + span = pin.tracer.current_span() + if span: + span.set_tag("hit", asbool(span.get_tag("hit") or "True") and hit) + span.set_tag("expired", asbool(span.get_tag("expired") or "False") or expired) + + instance.value_and_created_fn = wrapped_backend_fetcher diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/patch.py new file mode 100644 index 000000000..65cae90ad --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/patch.py @@ -0,0 +1,41 @@ +import dogpile + +from ddtrace.pin import Pin +from ddtrace.pin import _DD_PIN_NAME +from ddtrace.pin import _DD_PIN_PROXY_NAME +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from .lock import _wrap_lock_ctor +from .region import _wrap_get_create +from .region import _wrap_get_create_multi + + +_get_or_create = dogpile.cache.region.CacheRegion.get_or_create +_get_or_create_multi = dogpile.cache.region.CacheRegion.get_or_create_multi +_lock_ctor = dogpile.lock.Lock.__init__ + + +def patch(): + if getattr(dogpile.cache, "_datadog_patch", False): + return + setattr(dogpile.cache, "_datadog_patch", True) + + _w("dogpile.cache.region", "CacheRegion.get_or_create", _wrap_get_create) + _w("dogpile.cache.region", "CacheRegion.get_or_create_multi", _wrap_get_create_multi) + _w("dogpile.lock", "Lock.__init__", _wrap_lock_ctor) + + Pin(app="dogpile.cache", service="dogpile.cache").onto(dogpile.cache) + + +def unpatch(): + if not getattr(dogpile.cache, "_datadog_patch", False): + return + setattr(dogpile.cache, "_datadog_patch", False) + # This looks silly but the unwrap util doesn't support class instance methods, even + # though wrapt does. This was causing the patches to stack on top of each other + # during testing. + dogpile.cache.region.CacheRegion.get_or_create = _get_or_create + dogpile.cache.region.CacheRegion.get_or_create_multi = _get_or_create_multi + dogpile.lock.Lock.__init__ = _lock_ctor + setattr(dogpile.cache, _DD_PIN_NAME, None) + setattr(dogpile.cache, _DD_PIN_PROXY_NAME, None) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/region.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/region.py new file mode 100644 index 000000000..e60efa7c6 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/dogpile_cache/region.py @@ -0,0 +1,35 @@ +import dogpile + +from ddtrace.ext import SpanTypes + +from ...constants import SPAN_MEASURED_KEY +from ...pin import Pin +from ...utils import get_argument_value + + +def _wrap_get_create(func, instance, args, kwargs): + pin = Pin.get_from(dogpile.cache) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + key = get_argument_value(args, kwargs, 0, "key") + with pin.tracer.trace("dogpile.cache", resource="get_or_create", span_type=SpanTypes.CACHE) as span: + span.set_tag(SPAN_MEASURED_KEY) + span.set_tag("key", key) + span.set_tag("region", instance.name) + span.set_tag("backend", instance.actual_backend.__class__.__name__) + return func(*args, **kwargs) + + +def _wrap_get_create_multi(func, instance, args, kwargs): + pin = Pin.get_from(dogpile.cache) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + keys = get_argument_value(args, kwargs, 0, "keys") + with pin.tracer.trace("dogpile.cache", resource="get_or_create_multi", span_type="cache") as span: + span.set_tag(SPAN_MEASURED_KEY) + span.set_tag("keys", keys) + span.set_tag("region", instance.name) + span.set_tag("backend", instance.actual_backend.__class__.__name__) + return func(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/__init__.py new file mode 100644 index 000000000..8a5e1590f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/__init__.py @@ -0,0 +1,25 @@ +"""Instrument Elasticsearch to report Elasticsearch queries. + +``patch_all`` will automatically patch your Elasticsearch instance to make it work. +:: + + from ddtrace import Pin, patch + from elasticsearch import Elasticsearch + + # If not patched yet, you can patch elasticsearch specifically + patch(elasticsearch=True) + + # This will report spans with the default instrumentation + es = Elasticsearch(port=ELASTICSEARCH_CONFIG['port']) + # Example of instrumented query + es.indices.create(index='books', ignore=400) + + # Use a pin to specify metadata related to this client + es = Elasticsearch(port=ELASTICSEARCH_CONFIG['port']) + Pin.override(es.transport, service='elasticsearch-videos') + es.indices.create(index='videos', ignore=400) +""" +from .patch import patch + + +__all__ = ["patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..5b650d6d0 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..bbcd00498 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/__pycache__/quantize.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/__pycache__/quantize.cpython-38.pyc new file mode 100644 index 000000000..e9f1bbfb6 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/__pycache__/quantize.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/patch.py new file mode 100644 index 000000000..d063c4029 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/patch.py @@ -0,0 +1,120 @@ +from importlib import import_module + +from ddtrace import config +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import elasticsearch as metadata +from ...ext import http +from ...internal.compat import urlencode +from ...pin import Pin +from ...utils.wrappers import unwrap as _u +from .quantize import quantize + + +def _es_modules(): + module_names = ( + "elasticsearch", + "elasticsearch1", + "elasticsearch2", + "elasticsearch5", + "elasticsearch6", + "elasticsearch7", + ) + for module_name in module_names: + try: + yield import_module(module_name) + except ImportError: + pass + + +# NB: We are patching the default elasticsearch.transport module +def patch(): + for elasticsearch in _es_modules(): + _patch(elasticsearch) + + +def _patch(elasticsearch): + if getattr(elasticsearch, "_datadog_patch", False): + return + setattr(elasticsearch, "_datadog_patch", True) + _w(elasticsearch.transport, "Transport.perform_request", _get_perform_request(elasticsearch)) + Pin(service=metadata.SERVICE, app=metadata.APP).onto(elasticsearch.transport.Transport) + + +def unpatch(): + for elasticsearch in _es_modules(): + _unpatch(elasticsearch) + + +def _unpatch(elasticsearch): + if getattr(elasticsearch, "_datadog_patch", False): + setattr(elasticsearch, "_datadog_patch", False) + _u(elasticsearch.transport.Transport, "perform_request") + + +def _get_perform_request(elasticsearch): + def _perform_request(func, instance, args, kwargs): + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + with pin.tracer.trace("elasticsearch.query", span_type=SpanTypes.ELASTICSEARCH) as span: + span.set_tag(SPAN_MEASURED_KEY) + + # Don't instrument if the trace is not sampled + if not span.sampled: + return func(*args, **kwargs) + + method, url = args + params = kwargs.get("params") or {} + encoded_params = urlencode(params) + body = kwargs.get("body") + + span.service = pin.service + span.set_tag(metadata.METHOD, method) + span.set_tag(metadata.URL, url) + span.set_tag(metadata.PARAMS, encoded_params) + if config.elasticsearch.trace_query_string: + span.set_tag(http.QUERY_STRING, encoded_params) + + if method in ["GET", "POST"]: + span.set_tag(metadata.BODY, instance.serializer.dumps(body)) + status = None + + # set analytics sample rate + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.elasticsearch.get_analytics_sample_rate()) + + span = quantize(span) + + try: + result = func(*args, **kwargs) + except elasticsearch.exceptions.TransportError as e: + span.set_tag(http.STATUS_CODE, getattr(e, "status_code", 500)) + span.error = 1 + raise + + try: + # Optional metadata extraction with soft fail. + if isinstance(result, tuple) and len(result) == 2: + # elasticsearch<2.4; it returns both the status and the body + status, data = result + else: + # elasticsearch>=2.4; internal change for ``Transport.perform_request`` + # that just returns the body + data = result + + took = data.get("took") + if took: + span.set_metric(metadata.TOOK, int(took)) + except Exception: + pass + + if status: + span.set_tag(http.STATUS_CODE, status) + + return result + + return _perform_request diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/quantize.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/quantize.py new file mode 100644 index 000000000..5b526a3fa --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/elasticsearch/quantize.py @@ -0,0 +1,35 @@ +import re + +from ...ext import elasticsearch as metadata + + +# Replace any ID +ID_REGEXP = re.compile(r"/([0-9]+)([/\?]|$)") +ID_PLACEHOLDER = r"/?\2" + +# Remove digits from potential timestamped indexes (should be an option). +# For now, let's say 2+ digits +INDEX_REGEXP = re.compile(r"[0-9]{2,}") +INDEX_PLACEHOLDER = r"?" + + +def quantize(span): + """Quantize an elasticsearch span + + We want to extract a meaningful `resource` from the request. + We do it based on the method + url, with some cleanup applied to the URL. + + The URL might a ID, but also it is common to have timestamped indexes. + While the first is easy to catch, the second should probably be configurable. + + All of this should probably be done in the Agent. Later. + """ + url = span.get_tag(metadata.URL) + method = span.get_tag(metadata.METHOD) + + quantized_url = ID_REGEXP.sub(ID_PLACEHOLDER, url) + quantized_url = INDEX_REGEXP.sub(INDEX_PLACEHOLDER, quantized_url) + + span.resource = "{method} {url}".format(method=method, url=quantized_url) + + return span diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/__init__.py new file mode 100644 index 000000000..d67e1f715 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/__init__.py @@ -0,0 +1,57 @@ +""" +To trace the falcon web framework, install the trace middleware:: + + import falcon + from ddtrace import tracer + from ddtrace.contrib.falcon import TraceMiddleware + + mw = TraceMiddleware(tracer, 'my-falcon-app') + falcon.API(middleware=[mw]) + +You can also use the autopatching functionality:: + + import falcon + from ddtrace import tracer, patch + + patch(falcon=True) + + app = falcon.API() + +To disable distributed tracing when using autopatching, set the +``DATADOG_FALCON_DISTRIBUTED_TRACING`` environment variable to ``False``. + +**Supported span hooks** + +The following is a list of available tracer hooks that can be used to intercept +and modify spans created by this integration. + +- ``request`` + - Called before the response has been finished + - ``def on_falcon_request(span, request, response)`` + + +Example:: + + import falcon + from ddtrace import config, patch_all + patch_all() + + app = falcon.API() + + @config.falcon.hooks.on('request') + def on_falcon_request(span, request, response): + span.set_tag('my.custom', 'tag') + +:ref:`Headers tracing ` is supported for this integration. +""" +from ...utils.importlib import require_modules + + +required_modules = ["falcon"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .middleware import TraceMiddleware + from .patch import patch + + __all__ = ["TraceMiddleware", "patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..d2fc6913c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/__pycache__/middleware.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/__pycache__/middleware.cpython-38.pyc new file mode 100644 index 000000000..1b24b8233 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/__pycache__/middleware.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..3ac2a9a7d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/middleware.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/middleware.py new file mode 100644 index 000000000..9c4539237 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/middleware.py @@ -0,0 +1,103 @@ +import sys + +from ddtrace import config +from ddtrace.ext import SpanTypes +from ddtrace.ext import http as httpx + +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...internal.compat import iteritems + + +class TraceMiddleware(object): + def __init__(self, tracer, service="falcon", distributed_tracing=None): + # store tracing references + self.tracer = tracer + self.service = service + if distributed_tracing is not None: + config.falcon["distributed_tracing"] = distributed_tracing + + def process_request(self, req, resp): + # Falcon uppercases all header names. + headers = dict((k.lower(), v) for k, v in iteritems(req.headers)) + trace_utils.activate_distributed_headers(self.tracer, int_config=config.falcon, request_headers=headers) + + span = self.tracer.trace( + "falcon.request", + service=self.service, + span_type=SpanTypes.WEB, + ) + span.set_tag(SPAN_MEASURED_KEY) + + # set analytics sample rate with global config enabled + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.falcon.get_analytics_sample_rate(use_global_config=True)) + + trace_utils.set_http_meta( + span, config.falcon, method=req.method, url=req.url, query=req.query_string, request_headers=req.headers + ) + + def process_resource(self, req, resp, resource, params): + span = self.tracer.current_span() + if not span: + return # unexpected + span.resource = "%s %s" % (req.method, _name(resource)) + + def process_response(self, req, resp, resource, req_succeeded=None): + # req_succeded is not a kwarg in the API, but we need that to support + # Falcon 1.0 that doesn't provide this argument + span = self.tracer.current_span() + if not span: + return # unexpected + + status = resp.status.partition(" ")[0] + + # FIXME[matt] falcon does not map errors or unmatched routes + # to proper status codes, so we we have to try to infer them + # here. See https://github.com/falconry/falcon/issues/606 + if resource is None: + status = "404" + span.resource = "%s 404" % req.method + span.set_tag(httpx.STATUS_CODE, status) + span.finish() + return + + err_type = sys.exc_info()[0] + if err_type is not None: + if req_succeeded is None: + # backward-compatibility with Falcon 1.0; any version + # greater than 1.0 has req_succeded in [True, False] + # TODO[manu]: drop the support at some point + status = _detect_and_set_status_error(err_type, span) + elif req_succeeded is False: + # Falcon 1.1+ provides that argument that is set to False + # if get an Exception (404 is still an exception) + status = _detect_and_set_status_error(err_type, span) + + trace_utils.set_http_meta(span, config.falcon, status_code=status, response_headers=resp._headers) + + # Emit span hook for this response + # DEV: Emit before closing so they can overwrite `span.resource` if they want + config.falcon.hooks.emit("request", span, req, resp) + + # Close the span + span.finish() + + +def _is_404(err_type): + return "HTTPNotFound" in err_type.__name__ + + +def _detect_and_set_status_error(err_type, span): + """Detect the HTTP status code from the current stacktrace and + set the traceback to the given Span + """ + if not _is_404(err_type): + span.set_traceback() + return "500" + elif _is_404(err_type): + return "404" + + +def _name(r): + return "%s.%s" % (r.__module__, r.__class__.__name__) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/patch.py new file mode 100644 index 000000000..32bf3e5c8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/falcon/patch.py @@ -0,0 +1,46 @@ +import falcon + +from ddtrace import config +from ddtrace import tracer +from ddtrace.vendor import wrapt + +from ...utils.formats import asbool +from ...utils.formats import get_env +from ...utils.version import parse_version +from .middleware import TraceMiddleware + + +FALCON_VERSION = parse_version(falcon.__version__) + + +config._add( + "falcon", + dict( + distributed_tracing=asbool(get_env("falcon", "distributed_tracing", default=True)), + ), +) + + +def patch(): + """ + Patch falcon.API to include contrib.falcon.TraceMiddleware + by default + """ + if getattr(falcon, "_datadog_patch", False): + return + + setattr(falcon, "_datadog_patch", True) + if FALCON_VERSION >= (3, 0, 0): + wrapt.wrap_function_wrapper("falcon", "App.__init__", traced_init) + if FALCON_VERSION < (4, 0, 0): + wrapt.wrap_function_wrapper("falcon", "API.__init__", traced_init) + + +def traced_init(wrapped, instance, args, kwargs): + mw = kwargs.pop("middleware", []) + service = config._get_service(default="falcon") + + mw.insert(0, TraceMiddleware(tracer, service)) + kwargs["middleware"] = mw + + wrapped(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/fastapi/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/fastapi/__init__.py new file mode 100644 index 000000000..4519454a6 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/fastapi/__init__.py @@ -0,0 +1,66 @@ +""" +The fastapi integration will trace requests to and from FastAPI. + +Enabling +~~~~~~~~ + +The fastapi integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + from fastapi import FastAPI + + patch(fastapi=True) + app = FastAPI() + + +If using Python 3.6, the legacy ``AsyncioContextProvider`` will have to be +enabled before using the middleware:: + + from ddtrace.contrib.asyncio.provider import AsyncioContextProvider + from ddtrace import tracer # Or whichever tracer instance you plan to use + tracer.configure(context_provider=AsyncioContextProvider()) + + +Configuration +~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.fastapi['service_name'] + + The service name reported for your fastapi app. + + Can also be configured via the ``DD_SERVICE`` environment variable. + + Default: ``'fastapi'`` + +.. py:data:: ddtrace.config.fastapi['request_span_name'] + + The span name for a fastapi request. + + Default: ``'fastapi.request'`` + + +Example:: + + from ddtrace import config + + # Override service name + config.fastapi['service_name'] = 'custom-service-name' + + # Override request span name + config.fastapi['request_span_name'] = 'custom-request-span-name' + +""" +from ...utils.importlib import require_modules + + +required_modules = ["fastapi"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = ["patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/fastapi/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/fastapi/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..00e6e41ba Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/fastapi/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/fastapi/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/fastapi/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..005a9cc2b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/fastapi/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/fastapi/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/fastapi/patch.py new file mode 100644 index 000000000..99aca8a43 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/fastapi/patch.py @@ -0,0 +1,52 @@ +import fastapi +from fastapi.middleware import Middleware + +from ddtrace import config +from ddtrace.contrib.asgi.middleware import TraceMiddleware +from ddtrace.contrib.starlette.patch import get_resource +from ddtrace.internal.logger import get_logger +from ddtrace.utils.wrappers import unwrap as _u +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + + +log = get_logger(__name__) + +config._add( + "fastapi", + dict( + _default_service="fastapi", + request_span_name="fastapi.request", + distributed_tracing=True, + aggregate_resources=True, + ), +) + + +def span_modifier(span, scope): + resource = get_resource(scope) + if config.fastapi["aggregate_resources"] and resource: + span.resource = "{} {}".format(scope["method"], resource) + + +def traced_init(wrapped, instance, args, kwargs): + mw = kwargs.pop("middleware", []) + mw.insert(0, Middleware(TraceMiddleware, integration_config=config.fastapi, span_modifier=span_modifier)) + kwargs.update({"middleware": mw}) + wrapped(*args, **kwargs) + + +def patch(): + if getattr(fastapi, "_datadog_patch", False): + return + + setattr(fastapi, "_datadog_patch", True) + _w("fastapi.applications", "FastAPI.__init__", traced_init) + + +def unpatch(): + if not getattr(fastapi, "_datadog_patch", False): + return + + setattr(fastapi, "_datadog_patch", False) + + _u(fastapi.applications.FastAPI, "__init__") diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__init__.py new file mode 100644 index 000000000..597e037ae --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__init__.py @@ -0,0 +1,100 @@ +""" +The Flask__ integration will add tracing to all requests to your Flask application. + +This integration will track the entire Flask lifecycle including user-defined endpoints, hooks, +signals, and template rendering. + +To configure tracing manually:: + + from ddtrace import patch_all + patch_all() + + from flask import Flask + + app = Flask(__name__) + + + @app.route('/') + def index(): + return 'hello world' + + + if __name__ == '__main__': + app.run() + + +You may also enable Flask tracing automatically via ddtrace-run:: + + ddtrace-run python app.py + + +Configuration +~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.flask['distributed_tracing_enabled'] + + Whether to parse distributed tracing headers from requests received by your Flask app. + + Default: ``True`` + +.. py:data:: ddtrace.config.flask['service_name'] + + The service name reported for your Flask app. + + Can also be configured via the ``DD_SERVICE`` environment variable. + + Default: ``'flask'`` + +.. py:data:: ddtrace.config.flask['collect_view_args'] + + Whether to add request tags for view function argument values. + + Default: ``True`` + +.. py:data:: ddtrace.config.flask['template_default_name'] + + The default template name to use when one does not exist. + + Default: ```` + +.. py:data:: ddtrace.config.flask['trace_signals'] + + Whether to trace Flask signals (``before_request``, ``after_request``, etc). + + Default: ``True`` + + +Example:: + + from ddtrace import config + + # Enable distributed tracing + config.flask['distributed_tracing_enabled'] = True + + # Override service name + config.flask['service_name'] = 'custom-service-name' + + # Report 401, and 403 responses as errors + config.http_server.error_statuses = '401,403' + +.. __: http://flask.pocoo.org/ + +:ref:`All HTTP tags ` are supported for this integration. + +""" + +from ...utils.importlib import require_modules + + +required_modules = ["flask"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + # DEV: We do this so we can `@mock.patch('ddtrace.contrib.flask._patch.')` in tests + from . import patch as _patch + from .middleware import TraceMiddleware + + patch = _patch.patch + unpatch = _patch.unpatch + + __all__ = ["TraceMiddleware", "patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..8dbd92a09 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__pycache__/helpers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__pycache__/helpers.cpython-38.pyc new file mode 100644 index 000000000..03ec2c66e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__pycache__/helpers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__pycache__/middleware.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__pycache__/middleware.cpython-38.pyc new file mode 100644 index 000000000..50e0bcc8c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__pycache__/middleware.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..e01ab613e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__pycache__/wrappers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__pycache__/wrappers.cpython-38.pyc new file mode 100644 index 000000000..b3305cb7a Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/__pycache__/wrappers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/helpers.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/helpers.py new file mode 100644 index 000000000..78ab99112 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/helpers.py @@ -0,0 +1,38 @@ +import flask + +from ddtrace import Pin +from ddtrace import config + +from .. import trace_utils + + +def get_current_app(): + """Helper to get the flask.app.Flask from the current app context""" + appctx = flask._app_ctx_stack.top + if appctx: + return appctx.app + return None + + +def with_instance_pin(func): + """Helper to wrap a function wrapper and ensure an enabled pin is available for the `instance`""" + + def wrapper(wrapped, instance, args, kwargs): + pin = Pin._find(wrapped, instance, get_current_app()) + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + return func(pin, wrapped, instance, args, kwargs) + + return wrapper + + +def simple_tracer(name, span_type=None): + """Generate a simple tracer that wraps the function call with `with tracer.trace()`""" + + @with_instance_pin + def wrapper(pin, wrapped, instance, args, kwargs): + with pin.tracer.trace(name, service=trace_utils.int_service(pin, config.flask, pin), span_type=span_type): + return wrapped(*args, **kwargs) + + return wrapper diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/middleware.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/middleware.py new file mode 100644 index 000000000..c29da4ce8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/middleware.py @@ -0,0 +1,233 @@ +from flask import g +from flask import request +from flask import signals +import flask.templating + +from ddtrace import config +from ddtrace.constants import ERROR_MSG +from ddtrace.constants import ERROR_TYPE + +from .. import trace_utils +from ...ext import SpanTypes +from ...ext import http +from ...internal import compat +from ...internal.logger import get_logger +from ...utils.cache import cached +from ...utils.deprecation import deprecated + + +log = get_logger(__name__) + + +SPAN_NAME = "flask.request" + + +@cached() +def _normalize_resource(resource): + return compat.to_unicode(resource).lower() + + +class TraceMiddleware(object): + @deprecated(message="Use patching instead (see the docs).", version="1.0.0") + def __init__(self, app, tracer, service="flask", use_signals=True, distributed_tracing=None): + self.app = app + log.debug("flask: initializing trace middleware") + + # Attach settings to the inner application middleware. This is required if double + # instrumentation happens (i.e. `ddtrace-run` with `TraceMiddleware`). In that + # case, `ddtrace-run` instruments the application, but then users code is unable + # to update settings such as `distributed_tracing` flag. This step can be removed + # when the `Config` object is used + self.app._tracer = tracer + self.app._service = service + self.app._use_distributed_tracing = distributed_tracing + self.use_signals = use_signals + + # safe-guard to avoid double instrumentation + if getattr(app, "__dd_instrumentation", False): + return + setattr(app, "__dd_instrumentation", True) + + # Install hooks which time requests. + self.app.before_request(self._before_request) + self.app.after_request(self._after_request) + self.app.teardown_request(self._teardown_request) + + # Add exception handling signals. This will annotate exceptions that + # are caught and handled in custom user code. + # See https://github.com/DataDog/dd-trace-py/issues/390 + if use_signals and not signals.signals_available: + log.debug(_blinker_not_installed_msg) + self.use_signals = use_signals and signals.signals_available + timing_signals = { + "got_request_exception": self._request_exception, + } + self._receivers = [] + if self.use_signals and _signals_exist(timing_signals): + self._connect(timing_signals) + + _patch_render(tracer) + + def _connect(self, signal_to_handler): + connected = True + for name, handler in signal_to_handler.items(): + s = getattr(signals, name, None) + if s is None: + connected = False + log.warning("trying to instrument missing signal %s", name) + continue + # we should connect to the signal without using weak references + # otherwise they will be garbage collected and our handlers + # will be disconnected after the first call; for more details check: + # https://github.com/jek/blinker/blob/207446f2d97/blinker/base.py#L106-L108 + s.connect(handler, sender=self.app, weak=False) + self._receivers.append(handler) + return connected + + def _before_request(self): + """Starts tracing the current request and stores it in the global + request object. + """ + self._start_span() + + def _after_request(self, response): + """Runs after the server can process a response.""" + try: + self._process_response(response) + except Exception: + log.debug("flask: error tracing response", exc_info=True) + return response + + def _teardown_request(self, exception): + """Runs at the end of a request. If there's an unhandled exception, it + will be passed in. + """ + # when we teardown the span, ensure we have a clean slate. + span = g.__dict__.pop("flask_datadog_span", None) + if span is None: + return + + try: + self._finish_span(span, exception=exception) + except Exception: + log.debug("flask: error finishing span", exc_info=True) + + def _start_span(self): + trace_utils.activate_distributed_headers( + self.app._tracer, + int_config=config.flask, + request_headers=request.headers, + override=self.app._use_distributed_tracing, + ) + + try: + g.flask_datadog_span = self.app._tracer.trace( + SPAN_NAME, + service=self.app._service, + span_type=SpanTypes.WEB, + ) + except Exception: + log.debug("flask: error tracing request", exc_info=True) + + def _process_response(self, response): + span = getattr(g, "flask_datadog_span", None) + if not span: + return + + code = response.status_code if response else "" + trace_utils.set_http_meta(span, config.flask, status_code=code, response_headers=response.headers) + + def _request_exception(self, *args, **kwargs): + exception = kwargs.get("exception", None) + span = getattr(g, "flask_datadog_span", None) + if span and exception: + _set_error_on_span(span, exception) + + def _finish_span(self, span, exception=None): + if not span: + return + + code = span.get_tag(http.STATUS_CODE) or 0 + try: + code = int(code) + except Exception: + code = 0 + + if exception: + # if the request has already had a code set, don't override it. + code = code or 500 + _set_error_on_span(span, exception) + + # the endpoint that matched the request is None if an exception + # happened so we fallback to a common resource + span.error = 0 if code < 500 else 1 + + # the request isn't guaranteed to exist here, so only use it carefully. + method = "" + endpoint = "" + url = "" + query = "" + request_headers = "" + + if request: + method = request.method + endpoint = request.endpoint or code + url = request.base_url or "" + query = request.query_string.decode() if hasattr(request.query_string, "decode") else request.query_string + request_headers = request.headers + + # Let users specify their own resource in middleware if they so desire. + # See case https://github.com/DataDog/dd-trace-py/issues/353 + if span.resource == SPAN_NAME: + resource = endpoint or code + span.resource = _normalize_resource(resource) + + trace_utils.set_http_meta( + span, + config.flask, + method=method, + url=compat.to_unicode(url), + status_code=code, + query=query, + request_headers=request_headers, + ) + span.finish() + + +def _set_error_on_span(span, exception): + # The 3 next lines might not be strictly required, since `set_traceback` + # also get the exception from the sys.exc_info (and fill the error meta). + # Since we aren't sure it always work/for insuring no BC break, keep + # these lines which get overridden anyway. + span.set_tag(ERROR_TYPE, type(exception)) + span.set_tag(ERROR_MSG, exception) + # The provided `exception` object doesn't have a stack trace attached, + # so attach the stack trace with `set_traceback`. + span.set_traceback() + + +def _patch_render(tracer): + """patch flask's render template methods with the given tracer.""" + # fall back to patching global method + _render = flask.templating._render + + # If the method has already been patched and we're patching again then + # we have to patch again with the new tracer reference. + if hasattr(_render, "__dd_orig"): + _render = getattr(_render, "__dd_orig") + + def _traced_render(template, context, app): + with tracer.trace("flask.template", span_type=SpanTypes.TEMPLATE) as span: + span._set_str_tag("flask.template", compat.maybe_stringify(template.name) or "string") + return _render(template, context, app) + + setattr(_traced_render, "__dd_orig", _render) + flask.templating._render = _traced_render + + +def _signals_exist(names): + """Return true if all of the given signals exist in this version of flask.""" + return all(getattr(signals, n, False) for n in names) + + +_blinker_not_installed_msg = "please install blinker to use flask signals. " "http://flask.pocoo.org/docs/0.11/signals/" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/patch.py new file mode 100644 index 000000000..a510a8567 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/patch.py @@ -0,0 +1,520 @@ +import flask +import werkzeug + +from ddtrace import Pin +from ddtrace import config +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...internal.compat import maybe_stringify +from ...internal.logger import get_logger +from ...utils import get_argument_value +from ...utils.version import parse_version +from ...utils.wrappers import unwrap as _u +from .helpers import get_current_app +from .helpers import simple_tracer +from .helpers import with_instance_pin +from .wrappers import wrap_function +from .wrappers import wrap_signal + + +log = get_logger(__name__) + +FLASK_ENDPOINT = "flask.endpoint" +FLASK_VIEW_ARGS = "flask.view_args" +FLASK_URL_RULE = "flask.url_rule" +FLASK_VERSION = "flask.version" + +# Configure default configuration +config._add( + "flask", + dict( + # Flask service configuration + _default_service="flask", + app="flask", + collect_view_args=True, + distributed_tracing_enabled=True, + template_default_name="", + trace_signals=True, + ), +) + + +# Extract flask version into a tuple e.g. (0, 12, 1) or (1, 0, 2) +# DEV: This makes it so we can do `if flask_version >= (0, 12, 0):` +# DEV: Example tests: +# (0, 10, 0) > (0, 10) +# (0, 10, 0) >= (0, 10, 0) +# (0, 10, 1) >= (0, 10) +# (0, 11, 1) >= (0, 10) +# (0, 11, 1) >= (0, 10, 2) +# (1, 0, 0) >= (0, 10) +# (0, 9) == (0, 9) +# (0, 9, 0) != (0, 9) +# (0, 8, 5) <= (0, 9) +flask_version_str = getattr(flask, "__version__", "0.0.0") +flask_version = parse_version(flask_version_str) + + +def patch(): + """ + Patch `flask` module for tracing + """ + # Check to see if we have patched Flask yet or not + if getattr(flask, "_datadog_patch", False): + return + setattr(flask, "_datadog_patch", True) + + Pin().onto(flask.Flask) + + # flask.app.Flask methods that have custom tracing (add metadata, wrap functions, etc) + _w("flask", "Flask.wsgi_app", traced_wsgi_app) + _w("flask", "Flask.dispatch_request", request_tracer("dispatch_request")) + _w("flask", "Flask.preprocess_request", request_tracer("preprocess_request")) + _w("flask", "Flask.add_url_rule", traced_add_url_rule) + _w("flask", "Flask.endpoint", traced_endpoint) + if flask_version >= (2, 0, 0): + _w("flask", "Flask.register_error_handler", traced_register_error_handler) + else: + _w("flask", "Flask._register_error_handler", traced__register_error_handler) + + # flask.blueprints.Blueprint methods that have custom tracing (add metadata, wrap functions, etc) + _w("flask", "Blueprint.register", traced_blueprint_register) + _w("flask", "Blueprint.add_url_rule", traced_blueprint_add_url_rule) + + # flask.app.Flask traced hook decorators + flask_hooks = [ + "before_request", + "before_first_request", + "after_request", + "teardown_request", + "teardown_appcontext", + ] + for hook in flask_hooks: + _w("flask", "Flask.{}".format(hook), traced_flask_hook) + _w("flask", "after_this_request", traced_flask_hook) + + # flask.app.Flask traced methods + flask_app_traces = [ + "process_response", + "handle_exception", + "handle_http_exception", + "handle_user_exception", + "try_trigger_before_first_request_functions", + "do_teardown_request", + "do_teardown_appcontext", + "send_static_file", + ] + for name in flask_app_traces: + _w("flask", "Flask.{}".format(name), simple_tracer("flask.{}".format(name))) + + # flask static file helpers + _w("flask", "send_file", simple_tracer("flask.send_file")) + + # flask.json.jsonify + _w("flask", "jsonify", traced_jsonify) + + # flask.templating traced functions + _w("flask.templating", "_render", traced_render) + _w("flask", "render_template", traced_render_template) + _w("flask", "render_template_string", traced_render_template_string) + + # flask.blueprints.Blueprint traced hook decorators + bp_hooks = [ + "after_app_request", + "after_request", + "before_app_first_request", + "before_app_request", + "before_request", + "teardown_request", + "teardown_app_request", + ] + for hook in bp_hooks: + _w("flask", "Blueprint.{}".format(hook), traced_flask_hook) + + # flask.signals signals + if config.flask["trace_signals"]: + signals = [ + "template_rendered", + "request_started", + "request_finished", + "request_tearing_down", + "got_request_exception", + "appcontext_tearing_down", + ] + # These were added in 0.11.0 + if flask_version >= (0, 11): + signals.append("before_render_template") + + # These were added in 0.10.0 + if flask_version >= (0, 10): + signals.append("appcontext_pushed") + signals.append("appcontext_popped") + signals.append("message_flashed") + + for signal in signals: + module = "flask" + + # v0.9 missed importing `appcontext_tearing_down` in `flask/__init__.py` + # https://github.com/pallets/flask/blob/0.9/flask/__init__.py#L35-L37 + # https://github.com/pallets/flask/blob/0.9/flask/signals.py#L52 + # DEV: Version 0.9 doesn't have a patch version + if flask_version <= (0, 9) and signal == "appcontext_tearing_down": + module = "flask.signals" + + # DEV: Patch `receivers_for` instead of `connect` to ensure we don't mess with `disconnect` + _w(module, "{}.receivers_for".format(signal), traced_signal_receivers_for(signal)) + + +def unpatch(): + if not getattr(flask, "_datadog_patch", False): + return + setattr(flask, "_datadog_patch", False) + + props = [ + # Flask + "Flask.wsgi_app", + "Flask.dispatch_request", + "Flask.add_url_rule", + "Flask.endpoint", + "Flask.preprocess_request", + "Flask.process_response", + "Flask.handle_exception", + "Flask.handle_http_exception", + "Flask.handle_user_exception", + "Flask.try_trigger_before_first_request_functions", + "Flask.do_teardown_request", + "Flask.do_teardown_appcontext", + "Flask.send_static_file", + # Flask Hooks + "Flask.before_request", + "Flask.before_first_request", + "Flask.after_request", + "Flask.teardown_request", + "Flask.teardown_appcontext", + # Blueprint + "Blueprint.register", + "Blueprint.add_url_rule", + # Blueprint Hooks + "Blueprint.after_app_request", + "Blueprint.after_request", + "Blueprint.before_app_first_request", + "Blueprint.before_app_request", + "Blueprint.before_request", + "Blueprint.teardown_request", + "Blueprint.teardown_app_request", + # Signals + "template_rendered.receivers_for", + "request_started.receivers_for", + "request_finished.receivers_for", + "request_tearing_down.receivers_for", + "got_request_exception.receivers_for", + "appcontext_tearing_down.receivers_for", + # Top level props + "after_this_request", + "send_file", + "jsonify", + "render_template", + "render_template_string", + "templating._render", + ] + + if flask_version >= (2, 0, 0): + props.append("Flask.register_error_handler") + else: + props.append("Flask._register_error_handler") + + # These were added in 0.11.0 + if flask_version >= (0, 11): + props.append("before_render_template.receivers_for") + + # These were added in 0.10.0 + if flask_version >= (0, 10): + props.append("appcontext_pushed.receivers_for") + props.append("appcontext_popped.receivers_for") + props.append("message_flashed.receivers_for") + + for prop in props: + # Handle 'flask.request_started.receivers_for' + obj = flask + + # v0.9.0 missed importing `appcontext_tearing_down` in `flask/__init__.py` + # https://github.com/pallets/flask/blob/0.9/flask/__init__.py#L35-L37 + # https://github.com/pallets/flask/blob/0.9/flask/signals.py#L52 + # DEV: Version 0.9 doesn't have a patch version + if flask_version <= (0, 9) and prop == "appcontext_tearing_down.receivers_for": + obj = flask.signals + + if "." in prop: + attr, _, prop = prop.partition(".") + obj = getattr(obj, attr, object()) + _u(obj, prop) + + +# Wrap the `start_response` handler to extract response code +# DEV: We tried using `Flask.finalize_request`, which seemed to work, but gave us hell during tests +# DEV: The downside to using `start_response` is we do not have a `Flask.Response` object here, +# only `status_code`, and `headers` to work with +# On the bright side, this works in all versions of Flask (or any WSGI app actually) +def _wrap_start_response(func, span, request): + def traced_start_response(status_code, headers): + code, _, _ = status_code.partition(" ") + + # Override root span resource name to be ` 404` for 404 requests + # DEV: We do this because we want to make it easier to see all unknown requests together + # Also, we do this to reduce the cardinality on unknown urls + # DEV: If we have an endpoint or url rule tag, then we don't need to do this, + # we still want `GET /product/` grouped together, + # even if it is a 404 + if not span.get_tag(FLASK_ENDPOINT) and not span.get_tag(FLASK_URL_RULE): + span.resource = u" ".join((request.method, code)) + + trace_utils.set_http_meta(span, config.flask, status_code=code, response_headers=headers) + return func(status_code, headers) + + return traced_start_response + + +@with_instance_pin +def traced_wsgi_app(pin, wrapped, instance, args, kwargs): + """ + Wrapper for flask.app.Flask.wsgi_app + + This wrapper is the starting point for all requests. + """ + # DEV: This is safe before this is the args for a WSGI handler + # https://www.python.org/dev/peps/pep-3333/ + environ, start_response = args + + # Create a werkzeug request from the `environ` to make interacting with it easier + # DEV: This executes before a request context is created + request = werkzeug.Request(environ) + + # Configure distributed tracing + trace_utils.activate_distributed_headers(pin.tracer, int_config=config.flask, request_headers=request.headers) + + # Default resource is method and path: + # GET / + # POST /save + # We will override this below in `traced_dispatch_request` when we have a `RequestContext` and possibly a url rule + resource = u" ".join((request.method, request.path)) + with pin.tracer.trace( + "flask.request", + service=trace_utils.int_service(pin, config.flask), + resource=resource, + span_type=SpanTypes.WEB, + ) as span: + span.set_tag(SPAN_MEASURED_KEY) + # set analytics sample rate with global config enabled + sample_rate = config.flask.get_analytics_sample_rate(use_global_config=True) + if sample_rate is not None: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate) + + span._set_str_tag(FLASK_VERSION, flask_version_str) + + start_response = _wrap_start_response(start_response, span, request) + + # DEV: We set response status code in `_wrap_start_response` + # DEV: Use `request.base_url` and not `request.url` to keep from leaking any query string parameters + trace_utils.set_http_meta( + span, + config.flask, + method=request.method, + url=request.base_url, + query=request.query_string, + request_headers=request.headers, + ) + + return wrapped(environ, start_response) + + +def traced_blueprint_register(wrapped, instance, args, kwargs): + """ + Wrapper for flask.blueprints.Blueprint.register + + This wrapper just ensures the blueprint has a pin, either set manually on + itself from the user or inherited from the application + """ + app = get_argument_value(args, kwargs, 0, "app") + # Check if this Blueprint has a pin, otherwise clone the one from the app onto it + pin = Pin.get_from(instance) + if not pin: + pin = Pin.get_from(app) + if pin: + pin.clone().onto(instance) + return wrapped(*args, **kwargs) + + +def traced_blueprint_add_url_rule(wrapped, instance, args, kwargs): + pin = Pin._find(wrapped, instance) + if not pin: + return wrapped(*args, **kwargs) + + def _wrap(rule, endpoint=None, view_func=None, **kwargs): + if view_func: + pin.clone().onto(view_func) + return wrapped(rule, endpoint=endpoint, view_func=view_func, **kwargs) + + return _wrap(*args, **kwargs) + + +def traced_add_url_rule(wrapped, instance, args, kwargs): + """Wrapper for flask.app.Flask.add_url_rule to wrap all views attached to this app""" + + def _wrap(rule, endpoint=None, view_func=None, **kwargs): + if view_func: + # TODO: `if hasattr(view_func, 'view_class')` then this was generated from a `flask.views.View` + # should we do something special with these views? Change the name/resource? Add tags? + view_func = wrap_function(instance, view_func, name=endpoint, resource=rule) + + return wrapped(rule, endpoint=endpoint, view_func=view_func, **kwargs) + + return _wrap(*args, **kwargs) + + +def traced_endpoint(wrapped, instance, args, kwargs): + """Wrapper for flask.app.Flask.endpoint to ensure all endpoints are wrapped""" + endpoint = kwargs.get("endpoint", args[0]) + + def _wrapper(func): + # DEV: `wrap_function` will call `func_name(func)` for us + return wrapped(endpoint)(wrap_function(instance, func, resource=endpoint)) + + return _wrapper + + +def traced_flask_hook(wrapped, instance, args, kwargs): + """Wrapper for hook functions (before_request, after_request, etc) are properly traced""" + func = get_argument_value(args, kwargs, 0, "f") + return wrapped(wrap_function(instance, func)) + + +def traced_render_template(wrapped, instance, args, kwargs): + """Wrapper for flask.templating.render_template""" + pin = Pin._find(wrapped, instance, get_current_app()) + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + with pin.tracer.trace("flask.render_template", span_type=SpanTypes.TEMPLATE): + return wrapped(*args, **kwargs) + + +def traced_render_template_string(wrapped, instance, args, kwargs): + """Wrapper for flask.templating.render_template_string""" + pin = Pin._find(wrapped, instance, get_current_app()) + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + with pin.tracer.trace("flask.render_template_string", span_type=SpanTypes.TEMPLATE): + return wrapped(*args, **kwargs) + + +def traced_render(wrapped, instance, args, kwargs): + """ + Wrapper for flask.templating._render + + This wrapper is used for setting template tags on the span. + + This method is called for render_template or render_template_string + """ + pin = Pin._find(wrapped, instance, get_current_app()) + span = pin.tracer.current_span() + + if not pin.enabled or not span: + return wrapped(*args, **kwargs) + + def _wrap(template, context, app): + name = maybe_stringify(getattr(template, "name", None) or config.flask.get("template_default_name")) + if name is not None: + span.resource = name + span._set_str_tag("flask.template_name", name) + return wrapped(*args, **kwargs) + + return _wrap(*args, **kwargs) + + +def traced__register_error_handler(wrapped, instance, args, kwargs): + """Wrapper to trace all functions registered with flask.app._register_error_handler""" + + def _wrap(key, code_or_exception, f): + return wrapped(key, code_or_exception, wrap_function(instance, f)) + + return _wrap(*args, **kwargs) + + +def traced_register_error_handler(wrapped, instance, args, kwargs): + """Wrapper to trace all functions registered with flask.app.register_error_handler""" + + def _wrap(code_or_exception, f): + return wrapped(code_or_exception, wrap_function(instance, f)) + + return _wrap(*args, **kwargs) + + +def request_tracer(name): + @with_instance_pin + def _traced_request(pin, wrapped, instance, args, kwargs): + """ + Wrapper to trace a Flask function while trying to extract endpoint information + (endpoint, url_rule, view_args, etc) + + This wrapper will add identifier tags to the current span from `flask.app.Flask.wsgi_app`. + """ + span = pin.tracer.current_span() + if not pin.enabled or not span: + return wrapped(*args, **kwargs) + + try: + request = flask._request_ctx_stack.top.request + + # DEV: This name will include the blueprint name as well (e.g. `bp.index`) + if not span.get_tag(FLASK_ENDPOINT) and request.endpoint: + span.resource = u" ".join((request.method, request.endpoint)) + span._set_str_tag(FLASK_ENDPOINT, request.endpoint) + + if not span.get_tag(FLASK_URL_RULE) and request.url_rule and request.url_rule.rule: + span.resource = u" ".join((request.method, request.url_rule.rule)) + span._set_str_tag(FLASK_URL_RULE, request.url_rule.rule) + + if not span.get_tag(FLASK_VIEW_ARGS) and request.view_args and config.flask.get("collect_view_args"): + for k, v in request.view_args.items(): + # DEV: Do not use `_set_str_tag` here since view args can be string/int/float/path/uuid/etc + # https://flask.palletsprojects.com/en/1.1.x/api/#url-route-registrations + span.set_tag(u".".join((FLASK_VIEW_ARGS, k)), v) + except Exception: + log.debug('failed to set tags for "flask.request" span', exc_info=True) + + with pin.tracer.trace( + ".".join(("flask", name)), service=trace_utils.int_service(pin, config.flask, pin) + ) as request_span: + request_span._ignore_exception(werkzeug.exceptions.NotFound) + return wrapped(*args, **kwargs) + + return _traced_request + + +def traced_signal_receivers_for(signal): + """Wrapper for flask.signals.{signal}.receivers_for to ensure all signal receivers are traced""" + + def outer(wrapped, instance, args, kwargs): + sender = get_argument_value(args, kwargs, 0, "sender") + # See if they gave us the flask.app.Flask as the sender + app = None + if isinstance(sender, flask.Flask): + app = sender + for receiver in wrapped(*args, **kwargs): + yield wrap_signal(app, signal, receiver) + + return outer + + +def traced_jsonify(wrapped, instance, args, kwargs): + pin = Pin._find(wrapped, instance, get_current_app()) + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + with pin.tracer.trace("flask.jsonify"): + return wrapped(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/wrappers.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/wrappers.py new file mode 100644 index 000000000..47a9c1b83 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask/wrappers.py @@ -0,0 +1,48 @@ +from ddtrace import config +from ddtrace.vendor.wrapt import function_wrapper + +from .. import trace_utils +from ...pin import Pin +from ...utils.importlib import func_name +from .helpers import get_current_app + + +def wrap_function(instance, func, name=None, resource=None): + """ + Helper function to wrap common flask.app.Flask methods. + + This helper will first ensure that a Pin is available and enabled before tracing + """ + if not name: + name = func_name(func) + + @function_wrapper + def trace_func(wrapped, _instance, args, kwargs): + pin = Pin._find(wrapped, _instance, instance, get_current_app()) + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + with pin.tracer.trace(name, service=trace_utils.int_service(pin, config.flask), resource=resource): + return wrapped(*args, **kwargs) + + return trace_func(func) + + +def wrap_signal(app, signal, func): + """ + Helper used to wrap signal handlers + + We will attempt to find the pin attached to the flask.app.Flask app + """ + name = func_name(func) + + @function_wrapper + def trace_func(wrapped, instance, args, kwargs): + pin = Pin._find(wrapped, instance, app, get_current_app()) + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + with pin.tracer.trace(name, service=trace_utils.int_service(pin, config.flask)) as span: + span.set_tag("flask.signal", signal) + return wrapped(*args, **kwargs) + + return trace_func(func) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/__init__.py new file mode 100644 index 000000000..c5c302e51 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/__init__.py @@ -0,0 +1,56 @@ +""" +The flask cache tracer will track any access to a cache backend. +You can use this tracer together with the Flask tracer middleware. + +The tracer supports both `Flask-Cache `_ +and `Flask-Caching `_. + +To install the tracer, ``from ddtrace import tracer`` needs to be added:: + + from ddtrace import tracer + from ddtrace.contrib.flask_cache import get_traced_cache + +and the tracer needs to be initialized:: + + Cache = get_traced_cache(tracer, service='my-flask-cache-app') + +Here is the end result, in a sample app:: + + from flask import Flask + + from ddtrace import tracer + from ddtrace.contrib.flask_cache import get_traced_cache + + app = Flask(__name__) + + # get the traced Cache class + Cache = get_traced_cache(tracer, service='my-flask-cache-app') + + # use the Cache as usual with your preferred CACHE_TYPE + cache = Cache(app, config={'CACHE_TYPE': 'simple'}) + + def counter(): + # this access is traced + conn_counter = cache.get("conn_counter") + +Use a specific ``Cache`` implementation with:: + + from ddtrace import tracer + from ddtrace.contrib.flask_cache import get_traced_cache + + from flask_caching import Cache + + Cache = get_traced_cache(tracer, service='my-flask-cache-app', cache_cls=Cache) + +""" + +from ...utils.importlib import require_modules + + +required_modules = ["flask_cache", "flask_caching"] + +with require_modules(required_modules) as missing_modules: + if len(missing_modules) < len(required_modules): + from .tracers import get_traced_cache + + __all__ = ["get_traced_cache"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..1b2ceaa57 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/__pycache__/tracers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/__pycache__/tracers.cpython-38.pyc new file mode 100644 index 000000000..faa8b8722 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/__pycache__/tracers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/__pycache__/utils.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/__pycache__/utils.cpython-38.pyc new file mode 100644 index 000000000..43b5c7dd5 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/__pycache__/utils.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/tracers.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/tracers.py new file mode 100644 index 000000000..d1c623cfb --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/tracers.py @@ -0,0 +1,156 @@ +""" +Datadog trace code for flask_cache +""" + +import logging + +from ddtrace import config + +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from .utils import _extract_client +from .utils import _extract_conn_tags +from .utils import _resource_from_cache_prefix + + +log = logging.Logger(__name__) + +DEFAULT_SERVICE = config.service or "flask-cache" + +# standard tags +COMMAND_KEY = "flask_cache.key" +CACHE_BACKEND = "flask_cache.backend" +CONTACT_POINTS = "flask_cache.contact_points" + + +def get_traced_cache(ddtracer, service=DEFAULT_SERVICE, meta=None, cache_cls=None): + """ + Return a traced Cache object that behaves exactly as ``cache_cls``. + + ``cache_cls`` defaults to ``flask.ext.cache.Cache`` if Flask-Cache is installed + or ``flask_caching.Cache`` if flask-caching is installed. + """ + + if cache_cls is None: + # for compatibility reason, first check if flask_cache is present + try: + from flask.ext.cache import Cache + + cache_cls = Cache + except ImportError: + # use flask_caching if flask_cache is not present + from flask_caching import Cache + + cache_cls = Cache + + class TracedCache(cache_cls): + """ + Traced cache backend that monitors any operations done by flask_cache. Observed actions are: + * get, set, add, delete, clear + * all ``many_`` operations + """ + + _datadog_tracer = ddtracer + _datadog_service = service + _datadog_meta = meta + + def __trace(self, cmd, write=False): + """ + Start a tracing with default attributes and tags + """ + # create a new span + s = self._datadog_tracer.trace(cmd, span_type=SpanTypes.CACHE, service=self._datadog_service) + s.set_tag(SPAN_MEASURED_KEY) + # set span tags + s.set_tag(CACHE_BACKEND, self.config.get("CACHE_TYPE")) + s.set_tags(self._datadog_meta) + # set analytics sample rate + s.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.flask_cache.get_analytics_sample_rate()) + # add connection meta if there is one + client = _extract_client(self.cache, write=write) + if client is not None: + try: + s.set_tags(_extract_conn_tags(client)) + except Exception: + log.debug("error parsing connection tags", exc_info=True) + + return s + + def get(self, *args, **kwargs): + """ + Track ``get`` operation + """ + with self.__trace("flask_cache.cmd") as span: + span.resource = _resource_from_cache_prefix("GET", self.config) + if len(args) > 0: + span.set_tag(COMMAND_KEY, args[0]) + return super(TracedCache, self).get(*args, **kwargs) + + def set(self, *args, **kwargs): + """ + Track ``set`` operation + """ + with self.__trace("flask_cache.cmd", write=True) as span: + span.resource = _resource_from_cache_prefix("SET", self.config) + if len(args) > 0: + span.set_tag(COMMAND_KEY, args[0]) + return super(TracedCache, self).set(*args, **kwargs) + + def add(self, *args, **kwargs): + """ + Track ``add`` operation + """ + with self.__trace("flask_cache.cmd", write=True) as span: + span.resource = _resource_from_cache_prefix("ADD", self.config) + if len(args) > 0: + span.set_tag(COMMAND_KEY, args[0]) + return super(TracedCache, self).add(*args, **kwargs) + + def delete(self, *args, **kwargs): + """ + Track ``delete`` operation + """ + with self.__trace("flask_cache.cmd", write=True) as span: + span.resource = _resource_from_cache_prefix("DELETE", self.config) + if len(args) > 0: + span.set_tag(COMMAND_KEY, args[0]) + return super(TracedCache, self).delete(*args, **kwargs) + + def delete_many(self, *args, **kwargs): + """ + Track ``delete_many`` operation + """ + with self.__trace("flask_cache.cmd", write=True) as span: + span.resource = _resource_from_cache_prefix("DELETE_MANY", self.config) + span.set_tag(COMMAND_KEY, list(args)) + return super(TracedCache, self).delete_many(*args, **kwargs) + + def clear(self, *args, **kwargs): + """ + Track ``clear`` operation + """ + with self.__trace("flask_cache.cmd", write=True) as span: + span.resource = _resource_from_cache_prefix("CLEAR", self.config) + return super(TracedCache, self).clear(*args, **kwargs) + + def get_many(self, *args, **kwargs): + """ + Track ``get_many`` operation + """ + with self.__trace("flask_cache.cmd") as span: + span.resource = _resource_from_cache_prefix("GET_MANY", self.config) + span.set_tag(COMMAND_KEY, list(args)) + return super(TracedCache, self).get_many(*args, **kwargs) + + def set_many(self, *args, **kwargs): + """ + Track ``set_many`` operation + """ + with self.__trace("flask_cache.cmd", write=True) as span: + span.resource = _resource_from_cache_prefix("SET_MANY", self.config) + if len(args) > 0: + span.set_tag(COMMAND_KEY, list(args[0].keys())) + return super(TracedCache, self).set_many(*args, **kwargs) + + return TracedCache diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/utils.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/utils.py new file mode 100644 index 000000000..27a8f2215 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/flask_cache/utils.py @@ -0,0 +1,57 @@ +# project +from ...ext import net +from ..pylibmc.addrs import parse_addresses +from ..redis.util import _extract_conn_tags as extract_redis_tags + + +def _resource_from_cache_prefix(resource, cache): + """ + Combine the resource name with the cache prefix (if any) + """ + if getattr(cache, "key_prefix", None): + name = "{} {}".format(resource, cache.key_prefix) + else: + name = resource + + # enforce lowercase to make the output nicer to read + return name.lower() + + +def _extract_client(cache, write=False): + """ + Get the client from the cache instance according to the current operation + """ + client = getattr(cache, "_client", None) + if client is None: + # flask-caching has _read_clients & _write_client for the redis backend + client = getattr(cache, "_write_client" if write else "_read_clients", None) + return client + + +def _extract_conn_tags(client): + """ + For the given client extracts connection tags + """ + tags = {} + + if hasattr(client, "servers"): + # Memcached backend supports an address pool + if isinstance(client.servers, list) and len(client.servers) > 0: + # use the first address of the pool as a host because + # the code doesn't expose more information + contact_point = client.servers[0].address + tags[net.TARGET_HOST] = contact_point[0] + tags[net.TARGET_PORT] = contact_point[1] + elif hasattr(client, "connection_pool"): + # Redis main connection + redis_tags = extract_redis_tags(client.connection_pool.connection_kwargs) + tags.update(**redis_tags) + elif hasattr(client, "addresses"): + # pylibmc + # FIXME[matt] should we memoize this? + addrs = parse_addresses(client.addresses) + if addrs: + _, host, port, _ = addrs[0] + tags[net.TARGET_PORT] = port + tags[net.TARGET_HOST] = host + return tags diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/__init__.py new file mode 100644 index 000000000..9b89dddeb --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/__init__.py @@ -0,0 +1,31 @@ +""" +The ``futures`` integration propagates the current active tracing context +between threads. The integration ensures that when operations are executed +in a new thread, that thread can continue the previously generated trace. + + +Enabling +~~~~~~~~ + +The futures integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(futures=True) +""" +from ...utils.importlib import require_modules + + +required_modules = ["concurrent.futures"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = [ + "patch", + "unpatch", + ] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..2a028cb75 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..359cb254a Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/__pycache__/threading.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/__pycache__/threading.cpython-38.pyc new file mode 100644 index 000000000..1b3402ebf Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/__pycache__/threading.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/patch.py new file mode 100644 index 000000000..adad3565b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/patch.py @@ -0,0 +1,24 @@ +from concurrent import futures + +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from ...utils.wrappers import unwrap as _u +from .threading import _wrap_submit + + +def patch(): + """Enables Context Propagation between threads""" + if getattr(futures, "__datadog_patch", False): + return + setattr(futures, "__datadog_patch", True) + + _w("concurrent.futures", "ThreadPoolExecutor.submit", _wrap_submit) + + +def unpatch(): + """Disables Context Propagation between threads""" + if not getattr(futures, "__datadog_patch", False): + return + setattr(futures, "__datadog_patch", False) + + _u(futures.ThreadPoolExecutor, "submit") diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/threading.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/threading.py new file mode 100644 index 000000000..04f60baec --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/futures/threading.py @@ -0,0 +1,41 @@ +import ddtrace + + +def _wrap_submit(func, instance, args, kwargs): + """ + Wrap `Executor` method used to submit a work executed in another + thread. This wrapper ensures that a new `Context` is created and + properly propagated using an intermediate function. + """ + # If there isn't a currently active context, then do not create one + # DEV: Calling `.active()` when there isn't an active context will create a new context + # DEV: We need to do this in case they are either: + # - Starting nested futures + # - Starting futures from outside of an existing context + # + # In either of these cases we essentially will propagate the wrong context between futures + # + # The resolution is to not create/propagate a new context if one does not exist, but let the + # future's thread create the context instead. + current_ctx = None + if ddtrace.tracer.context_provider._has_active_context(): + current_ctx = ddtrace.tracer.context_provider.active() + + # extract the target function that must be executed in + # a new thread and the `target` arguments + fn = args[0] + fn_args = args[1:] + return func(_wrap_execution, current_ctx, fn, fn_args, kwargs) + + +def _wrap_execution(ctx, fn, args, kwargs): + """ + Intermediate target function that is executed in a new thread; + it receives the original function with arguments and keyword + arguments, including our tracing `Context`. The current context + provider sets the Active context in a thread local storage + variable because it's outside the asynchronous loop. + """ + if ctx is not None: + ddtrace.tracer.context_provider.activate(ctx) + return fn(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/__init__.py new file mode 100644 index 000000000..78483e9f7 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/__init__.py @@ -0,0 +1,74 @@ +""" +The gevent integration adds support for tracing across greenlets. + + +The integration patches the gevent internals to add context management logic. + +.. note:: + If :ref:`ddtrace-run` is being used set ``DD_GEVENT_PATCH_ALL=true`` and + ``gevent.monkey.patch_all()`` will be called as early as possible in the application + to avoid patching conflicts. + If ``ddtrace-run`` is not being used then be sure to call ``gevent.monkey.patch_all`` + before importing ``ddtrace`` and calling ``ddtrace.patch`` or `ddtrace.patch_all``. + + +The integration also configures the global tracer instance to use a gevent +context provider to utilize the context management logic. + +If custom tracer instances are being used in a gevent application, then +configure it with:: + + from ddtrace.contrib.gevent import context_provider + + tracer.configure(context_provider=context_provider) + + +Enabling +~~~~~~~~ + +The integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(gevent=True) + + +**Note:** these calls need to be performed before calling the gevent patching. + +Example of the context propagation:: + + def my_parent_function(): + with tracer.trace("web.request") as span: + span.service = "web" + gevent.spawn(worker_function) + + + def worker_function(): + # then trace its child + with tracer.trace("greenlet.call") as span: + span.service = "greenlet" + ... + + with tracer.trace("greenlet.child_call") as child: + ... +""" +from ...utils.importlib import require_modules + + +required_modules = ["gevent"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + from .provider import GeventContextProvider + + context_provider = GeventContextProvider() + + __all__ = [ + "patch", + "unpatch", + "context_provider", + ] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..276c580cf Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/__pycache__/greenlet.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/__pycache__/greenlet.cpython-38.pyc new file mode 100644 index 000000000..f38137fd2 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/__pycache__/greenlet.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..63d962918 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/__pycache__/provider.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/__pycache__/provider.cpython-38.pyc new file mode 100644 index 000000000..9f9f33979 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/__pycache__/provider.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/greenlet.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/greenlet.py new file mode 100644 index 000000000..614096488 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/greenlet.py @@ -0,0 +1,60 @@ +import gevent +import gevent.pool as gpool + +from .provider import GeventContextProvider + + +GEVENT_VERSION = gevent.version_info[0:3] + + +class TracingMixin(object): + def __init__(self, *args, **kwargs): + # get the active context/span if available + current_g = gevent.getcurrent() + ctx = getattr(current_g, GeventContextProvider._CONTEXT_ATTR, None) + + # create the Greenlet as usual + super(TracingMixin, self).__init__(*args, **kwargs) + + # copy the active span/context into the new greenlet + if ctx: + setattr(self, GeventContextProvider._CONTEXT_ATTR, ctx) + + +class TracedGreenlet(TracingMixin, gevent.Greenlet): + """ + ``Greenlet`` class that is used to replace the original ``gevent`` + class. This class is supposed to do ``Context`` replacing operation, so + that any greenlet inherits the context from the parent Greenlet. + When a new greenlet is spawned from the main greenlet, a new instance + of ``Context`` is created. The main greenlet is not affected by this behavior. + + There is no need to inherit this class to create or optimize greenlets + instances, because this class replaces ``gevent.greenlet.Greenlet`` + through the ``patch()`` method. After the patch, extending the gevent + ``Greenlet`` class means extending automatically ``TracedGreenlet``. + """ + + def __init__(self, *args, **kwargs): + super(TracedGreenlet, self).__init__(*args, **kwargs) + + +class TracedIMapUnordered(TracingMixin, gpool.IMapUnordered): + def __init__(self, *args, **kwargs): + super(TracedIMapUnordered, self).__init__(*args, **kwargs) + + +if GEVENT_VERSION >= (1, 3) or GEVENT_VERSION < (1, 1): + # For gevent <1.1 and >=1.3, IMap is its own class, so we derive + # from TracingMixin + class TracedIMap(TracingMixin, gpool.IMap): + def __init__(self, *args, **kwargs): + super(TracedIMap, self).__init__(*args, **kwargs) + + +else: + # For gevent >=1.1 and <1.3, IMap derives from IMapUnordered, so we derive + # from TracedIMapUnordered and get tracing that way + class TracedIMap(gpool.IMap, TracedIMapUnordered): + def __init__(self, *args, **kwargs): + super(TracedIMap, self).__init__(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/patch.py new file mode 100644 index 000000000..0db7ec0ce --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/patch.py @@ -0,0 +1,67 @@ +import gevent +import gevent.pool + +import ddtrace + +from ...provider import DefaultContextProvider +from .greenlet import GEVENT_VERSION +from .greenlet import TracedGreenlet +from .greenlet import TracedIMap +from .greenlet import TracedIMapUnordered +from .provider import GeventContextProvider + + +__Greenlet = gevent.Greenlet +__IMap = gevent.pool.IMap +__IMapUnordered = gevent.pool.IMapUnordered + + +def patch(): + """ + Patch the gevent module so that all references to the + internal ``Greenlet`` class points to the ``DatadogGreenlet`` + class. + + This action ensures that if a user extends the ``Greenlet`` + class, the ``TracedGreenlet`` is used as a parent class. + """ + _replace(TracedGreenlet, TracedIMap, TracedIMapUnordered) + ddtrace.tracer.configure(context_provider=GeventContextProvider()) + + +def unpatch(): + """ + Restore the original ``Greenlet``. This function must be invoked + before executing application code, otherwise the ``DatadogGreenlet`` + class may be used during initialization. + """ + _replace(__Greenlet, __IMap, __IMapUnordered) + ddtrace.tracer.configure(context_provider=DefaultContextProvider()) + + +def _replace(g_class, imap_class, imap_unordered_class): + """ + Utility function that replace the gevent Greenlet class with the given one. + """ + # replace the original Greenlet classes with the new one + gevent.greenlet.Greenlet = g_class + + if GEVENT_VERSION >= (1, 3): + # For gevent >= 1.3.0, IMap and IMapUnordered were pulled out of + # gevent.pool and into gevent._imap + gevent._imap.IMap = imap_class + gevent._imap.IMapUnordered = imap_unordered_class + gevent.pool.IMap = gevent._imap.IMap + gevent.pool.IMapUnordered = gevent._imap.IMapUnordered + gevent.pool.Greenlet = gevent.greenlet.Greenlet + else: + # For gevent < 1.3, only patching of gevent.pool classes necessary + gevent.pool.IMap = imap_class + gevent.pool.IMapUnordered = imap_unordered_class + + gevent.pool.Group.greenlet_class = g_class + + # replace gevent shortcuts + gevent.Greenlet = gevent.greenlet.Greenlet + gevent.spawn = gevent.greenlet.Greenlet.spawn + gevent.spawn_later = gevent.greenlet.Greenlet.spawn_later diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/provider.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/provider.py new file mode 100644 index 000000000..13b05d3f6 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/gevent/provider.py @@ -0,0 +1,42 @@ +import gevent + +from ...provider import BaseContextProvider +from ...provider import DatadogContextMixin +from ...span import Span + + +class GeventContextProvider(BaseContextProvider, DatadogContextMixin): + """Manages the active context for gevent execution. + + This provider depends on corresponding monkey patches to copy the active + context from one greenlet to another. + """ + + # Greenlet attribute used to set/get the context + _CONTEXT_ATTR = "__datadog_context" + + def _get_current_context(self): + """Helper to get the active context from the current greenlet.""" + current_g = gevent.getcurrent() + if current_g is not None: + return getattr(current_g, self._CONTEXT_ATTR, None) + return None + + def _has_active_context(self): + """Helper to determine if there is an active context.""" + return self._get_current_context() is not None + + def activate(self, context): + """Sets the active context for the current running ``Greenlet``.""" + current_g = gevent.getcurrent() + if current_g is not None: + setattr(current_g, self._CONTEXT_ATTR, context) + super(GeventContextProvider, self).activate(context) + return context + + def active(self): + """Returns the active context for this execution flow.""" + ctx = self._get_current_context() + if isinstance(ctx, Span): + return self._update_active(ctx) + return ctx diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__init__.py new file mode 100644 index 000000000..357f1b184 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__init__.py @@ -0,0 +1,88 @@ +""" +The gRPC integration traces the client and server using the interceptor pattern. + + +Enabling +~~~~~~~~ + +The gRPC integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(grpc=True) + + # use grpc like usual + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.grpc["service"] + + The service name reported by default for gRPC client instances. + + This option can also be set with the ``DD_GRPC_SERVICE`` environment + variable. + + Default: ``"grpc-client"`` + +.. py:data:: ddtrace.config.grpc_server["service"] + + The service name reported by default for gRPC server instances. + + This option can also be set with the ``DD_SERVICE`` or + ``DD_GRPC_SERVER_SERVICE`` environment variables. + + Default: ``"grpc-server"`` + + +Instance Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +To configure the gRPC integration on an per-channel basis use the +``Pin`` API:: + + import grpc + from ddtrace import Pin, patch, Tracer + + patch(grpc=True) + custom_tracer = Tracer() + + # override the pin on the client + Pin.override(grpc.Channel, service='mygrpc', tracer=custom_tracer) + with grpc.insecure_channel('localhost:50051') as channel: + # create stubs and send requests + pass + +To configure the gRPC integration on the server use the ``Pin`` API:: + + import grpc + from grpc.framework.foundation import logging_pool + + from ddtrace import Pin, patch, Tracer + + patch(grpc=True) + custom_tracer = Tracer() + + # override the pin on the server + Pin.override(grpc.Server, service='mygrpc', tracer=custom_tracer) + server = grpc.server(logging_pool.pool(2)) + server.add_insecure_port('localhost:50051') + add_MyServicer_to_server(MyServicer(), server) + server.start() +""" + + +from ...utils.importlib import require_modules + + +required_modules = ["grpc"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = ["patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..778761228 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/client_interceptor.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/client_interceptor.cpython-38.pyc new file mode 100644 index 000000000..91e779242 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/client_interceptor.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/constants.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/constants.cpython-38.pyc new file mode 100644 index 000000000..8efa1051b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/constants.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..1e7587368 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/server_interceptor.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/server_interceptor.cpython-38.pyc new file mode 100644 index 000000000..6a4289ca2 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/server_interceptor.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/utils.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/utils.cpython-38.pyc new file mode 100644 index 000000000..0bb220fe9 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/__pycache__/utils.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/client_interceptor.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/client_interceptor.py new file mode 100644 index 000000000..7f433fbae --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/client_interceptor.py @@ -0,0 +1,265 @@ +import collections + +import grpc + +from ddtrace import config +from ddtrace.ext import SpanTypes +from ddtrace.internal.compat import stringify +from ddtrace.internal.compat import to_unicode +from ddtrace.vendor import wrapt + +from . import constants +from . import utils +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import ERROR_MSG +from ...constants import ERROR_STACK +from ...constants import ERROR_TYPE +from ...constants import SPAN_MEASURED_KEY +from ...internal.logger import get_logger +from ...propagation.http import HTTPPropagator + + +log = get_logger(__name__) + +# DEV: Follows Python interceptors RFC laid out in +# https://github.com/grpc/proposal/blob/master/L13-python-interceptors.md + +# DEV: __version__ added in v1.21.4 +# https://github.com/grpc/grpc/commit/dd4830eae80143f5b0a9a3a1a024af4cf60e7d02 + + +def create_client_interceptor(pin, host, port): + return _ClientInterceptor(pin, host, port) + + +def intercept_channel(wrapped, instance, args, kwargs): + channel = args[0] + interceptors = args[1:] + if isinstance(getattr(channel, "_interceptor", None), _ClientInterceptor): + dd_interceptor = channel._interceptor + base_channel = getattr(channel, "_channel", None) + if base_channel: + new_channel = wrapped(channel._channel, *interceptors) + return grpc.intercept_channel(new_channel, dd_interceptor) + + return wrapped(*args, **kwargs) + + +class _ClientCallDetails( + collections.namedtuple("_ClientCallDetails", ("method", "timeout", "metadata", "credentials")), + grpc.ClientCallDetails, +): + pass + + +def _future_done_callback(span): + def func(response): + try: + # pull out response code from gRPC response to use both for `grpc.status.code` + # tag and the error type tag if the response is an exception + response_code = response.code() + # cast code to unicode for tags + status_code = to_unicode(response_code) + span._set_str_tag(constants.GRPC_STATUS_CODE_KEY, status_code) + + if response_code != grpc.StatusCode.OK: + _handle_error(span, response, status_code) + finally: + span.finish() + + return func + + +def _handle_response(span, response): + # use duck-typing to support future-like response as in the case of + # google-api-core which has its own future base class + # https://github.com/googleapis/python-api-core/blob/49c6755a21215bbb457b60db91bab098185b77da/google/api_core/future/base.py#L23 + if hasattr(response, "add_done_callback"): + response.add_done_callback(_future_done_callback(span)) + + +def _handle_error(span, response_error, status_code): + # response_error should be a grpc.Future and so we expect to have cancelled(), + # exception() and traceback() methods if a computation has resulted in an + # exception being raised + if ( + not callable(getattr(response_error, "cancelled", None)) + and not callable(getattr(response_error, "exception", None)) + and not callable(getattr(response_error, "traceback", None)) + ): + return + + if response_error.cancelled(): + # handle cancelled futures separately to avoid raising grpc.FutureCancelledError + span.error = 1 + exc_val = to_unicode(response_error.details()) + span._set_str_tag(ERROR_MSG, exc_val) + span._set_str_tag(ERROR_TYPE, status_code) + return + + exception = response_error.exception() + traceback = response_error.traceback() + + if exception is not None and traceback is not None: + span.error = 1 + if isinstance(exception, grpc.RpcError): + # handle internal gRPC exceptions separately to get status code and + # details as tags properly + exc_val = to_unicode(response_error.details()) + span._set_str_tag(ERROR_MSG, exc_val) + span._set_str_tag(ERROR_TYPE, status_code) + span._set_str_tag(ERROR_STACK, stringify(traceback)) + else: + exc_type = type(exception) + span.set_exc_info(exc_type, exception, traceback) + status_code = to_unicode(response_error.code()) + + +class _WrappedResponseCallFuture(wrapt.ObjectProxy): + def __init__(self, wrapped, span): + super(_WrappedResponseCallFuture, self).__init__(wrapped) + self._span = span + # Registers callback on the _MultiThreadedRendezvous future to finish + # span in case StopIteration is never raised but RPC is terminated + _handle_response(self._span, self.__wrapped__) + + def __iter__(self): + return self + + def _next(self): + # While an iterator ObjectProxy requires only __iter__ and __next__, we + # make sure to also proxy the grpc._channel._Rendezvous._next method in + # case it is being called directly as in google.api_core.grpc_helpers + # and grpc_gcp._channel. + # https://github.com/grpc/grpc/blob/5195a06ddea8da6603c6672e0ed09fec9b5c16ac/src/python/grpcio/grpc/_channel.py#L418-L419 + # https://github.com/googleapis/python-api-core/blob/35e87e0aca52167029784379ca84e979098e1d6c/google/api_core/grpc_helpers.py#L84 + # https://github.com/GoogleCloudPlatform/grpc-gcp-python/blob/5a2cd9807bbaf1b85402a2a364775e5b65853df6/src/grpc_gcp/_channel.py#L102 + try: + return next(self.__wrapped__) + except StopIteration: + # Callback will handle span finishing + raise + except grpc.RpcError as rpc_error: + # DEV: grpcio<1.18.0 grpc.RpcError is raised rather than returned as response + # https://github.com/grpc/grpc/commit/8199aff7a66460fbc4e9a82ade2e95ef076fd8f9 + # handle as a response + _handle_response(self._span, rpc_error) + raise + except Exception: + # DEV: added for safety though should not be reached since wrapped response + log.debug("unexpected non-grpc exception raised, closing open span", exc_info=True) + self._span.set_traceback() + self._span.finish() + raise + + def __next__(self): + return self._next() + + next = __next__ + + +class _ClientInterceptor( + grpc.UnaryUnaryClientInterceptor, + grpc.UnaryStreamClientInterceptor, + grpc.StreamUnaryClientInterceptor, + grpc.StreamStreamClientInterceptor, +): + def __init__(self, pin, host, port): + self._pin = pin + self._host = host + self._port = port + + def _intercept_client_call(self, method_kind, client_call_details): + tracer = self._pin.tracer + + span = tracer.trace( + "grpc", + span_type=SpanTypes.GRPC, + service=trace_utils.ext_service(self._pin, config.grpc), + resource=client_call_details.method, + ) + span.set_tag(SPAN_MEASURED_KEY) + + utils.set_grpc_method_meta(span, client_call_details.method, method_kind) + utils.set_grpc_client_meta(span, self._host, self._port) + span._set_str_tag(constants.GRPC_SPAN_KIND_KEY, constants.GRPC_SPAN_KIND_VALUE_CLIENT) + + sample_rate = config.grpc.get_analytics_sample_rate() + if sample_rate is not None: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate) + + # inject tags from pin + if self._pin.tags: + span.set_tags(self._pin.tags) + + # propagate distributed tracing headers if available + headers = {} + if config.grpc.distributed_tracing_enabled: + HTTPPropagator.inject(span.context, headers) + + metadata = [] + if client_call_details.metadata is not None: + metadata = list(client_call_details.metadata) + metadata.extend(headers.items()) + + client_call_details = _ClientCallDetails( + client_call_details.method, + client_call_details.timeout, + metadata, + client_call_details.credentials, + ) + + return span, client_call_details + + def intercept_unary_unary(self, continuation, client_call_details, request): + span, client_call_details = self._intercept_client_call( + constants.GRPC_METHOD_KIND_UNARY, + client_call_details, + ) + try: + response = continuation(client_call_details, request) + _handle_response(span, response) + except grpc.RpcError as rpc_error: + # DEV: grpcio<1.18.0 grpc.RpcError is raised rather than returned as response + # https://github.com/grpc/grpc/commit/8199aff7a66460fbc4e9a82ade2e95ef076fd8f9 + # handle as a response + _handle_response(span, rpc_error) + raise + + return response + + def intercept_unary_stream(self, continuation, client_call_details, request): + span, client_call_details = self._intercept_client_call( + constants.GRPC_METHOD_KIND_SERVER_STREAMING, + client_call_details, + ) + response_iterator = continuation(client_call_details, request) + response_iterator = _WrappedResponseCallFuture(response_iterator, span) + return response_iterator + + def intercept_stream_unary(self, continuation, client_call_details, request_iterator): + span, client_call_details = self._intercept_client_call( + constants.GRPC_METHOD_KIND_CLIENT_STREAMING, + client_call_details, + ) + try: + response = continuation(client_call_details, request_iterator) + _handle_response(span, response) + except grpc.RpcError as rpc_error: + # DEV: grpcio<1.18.0 grpc.RpcError is raised rather than returned as response + # https://github.com/grpc/grpc/commit/8199aff7a66460fbc4e9a82ade2e95ef076fd8f9 + # handle as a response + _handle_response(span, rpc_error) + raise + + return response + + def intercept_stream_stream(self, continuation, client_call_details, request_iterator): + span, client_call_details = self._intercept_client_call( + constants.GRPC_METHOD_KIND_BIDI_STREAMING, + client_call_details, + ) + response_iterator = continuation(client_call_details, request_iterator) + response_iterator = _WrappedResponseCallFuture(response_iterator, span) + return response_iterator diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/constants.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/constants.py new file mode 100644 index 000000000..385013ba3 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/constants.py @@ -0,0 +1,24 @@ +import grpc + + +GRPC_PIN_MODULE_SERVER = grpc.Server +GRPC_PIN_MODULE_CLIENT = grpc.Channel +GRPC_METHOD_PATH_KEY = "grpc.method.path" +GRPC_METHOD_PACKAGE_KEY = "grpc.method.package" +GRPC_METHOD_SERVICE_KEY = "grpc.method.service" +GRPC_METHOD_NAME_KEY = "grpc.method.name" +GRPC_METHOD_KIND_KEY = "grpc.method.kind" +GRPC_STATUS_CODE_KEY = "grpc.status.code" +GRPC_REQUEST_METADATA_PREFIX_KEY = "grpc.request.metadata." +GRPC_RESPONSE_METADATA_PREFIX_KEY = "grpc.response.metadata." +GRPC_HOST_KEY = "grpc.host" +GRPC_PORT_KEY = "grpc.port" +GRPC_SPAN_KIND_KEY = "span.kind" +GRPC_SPAN_KIND_VALUE_CLIENT = "client" +GRPC_SPAN_KIND_VALUE_SERVER = "server" +GRPC_METHOD_KIND_UNARY = "unary" +GRPC_METHOD_KIND_CLIENT_STREAMING = "client_streaming" +GRPC_METHOD_KIND_SERVER_STREAMING = "server_streaming" +GRPC_METHOD_KIND_BIDI_STREAMING = "bidi_streaming" +GRPC_SERVICE_SERVER = "grpc-server" +GRPC_SERVICE_CLIENT = "grpc-client" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/patch.py new file mode 100644 index 000000000..d39519a4f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/patch.py @@ -0,0 +1,121 @@ +import grpc + +from ddtrace import Pin +from ddtrace import config +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from . import constants +from . import utils +from ...utils.wrappers import unwrap as _u +from .client_interceptor import create_client_interceptor +from .client_interceptor import intercept_channel +from .server_interceptor import create_server_interceptor + + +config._add( + "grpc_server", + dict( + _default_service=constants.GRPC_SERVICE_SERVER, + distributed_tracing_enabled=True, + ), +) + + +# TODO[tbutt]: keeping name for client config unchanged to maintain backwards +# compatibility but should change in future +config._add( + "grpc", + dict( + _default_service=constants.GRPC_SERVICE_CLIENT, + distributed_tracing_enabled=True, + ), +) + + +def patch(): + _patch_client() + _patch_server() + + +def unpatch(): + _unpatch_client() + _unpatch_server() + + +def _patch_client(): + if getattr(constants.GRPC_PIN_MODULE_CLIENT, "__datadog_patch", False): + return + setattr(constants.GRPC_PIN_MODULE_CLIENT, "__datadog_patch", True) + + Pin().onto(constants.GRPC_PIN_MODULE_CLIENT) + + _w("grpc", "insecure_channel", _client_channel_interceptor) + _w("grpc", "secure_channel", _client_channel_interceptor) + _w("grpc", "intercept_channel", intercept_channel) + + +def _unpatch_client(): + if not getattr(constants.GRPC_PIN_MODULE_CLIENT, "__datadog_patch", False): + return + setattr(constants.GRPC_PIN_MODULE_CLIENT, "__datadog_patch", False) + + pin = Pin.get_from(constants.GRPC_PIN_MODULE_CLIENT) + if pin: + pin.remove_from(constants.GRPC_PIN_MODULE_CLIENT) + + _u(grpc, "secure_channel") + _u(grpc, "insecure_channel") + + +def _patch_server(): + if getattr(constants.GRPC_PIN_MODULE_SERVER, "__datadog_patch", False): + return + setattr(constants.GRPC_PIN_MODULE_SERVER, "__datadog_patch", True) + + Pin().onto(constants.GRPC_PIN_MODULE_SERVER) + + _w("grpc", "server", _server_constructor_interceptor) + + +def _unpatch_server(): + if not getattr(constants.GRPC_PIN_MODULE_SERVER, "__datadog_patch", False): + return + setattr(constants.GRPC_PIN_MODULE_SERVER, "__datadog_patch", False) + + pin = Pin.get_from(constants.GRPC_PIN_MODULE_SERVER) + if pin: + pin.remove_from(constants.GRPC_PIN_MODULE_SERVER) + + _u(grpc, "server") + + +def _client_channel_interceptor(wrapped, instance, args, kwargs): + channel = wrapped(*args, **kwargs) + + pin = Pin.get_from(channel) + if not pin or not pin.enabled(): + return channel + + (host, port) = utils._parse_target_from_args(args, kwargs) + + interceptor_function = create_client_interceptor(pin, host, port) + return grpc.intercept_channel(channel, interceptor_function) + + +def _server_constructor_interceptor(wrapped, instance, args, kwargs): + # DEV: we clone the pin on the grpc module and configure it for the server + # interceptor + + pin = Pin.get_from(constants.GRPC_PIN_MODULE_SERVER) + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + interceptor = create_server_interceptor(pin) + + # DEV: Inject our tracing interceptor first in the list of interceptors + if "interceptors" in kwargs: + kwargs["interceptors"] = (interceptor,) + tuple(kwargs["interceptors"]) + else: + kwargs["interceptors"] = (interceptor,) + + return wrapped(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/server_interceptor.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/server_interceptor.py new file mode 100644 index 000000000..368f177c7 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/server_interceptor.py @@ -0,0 +1,124 @@ +import grpc + +from ddtrace import config +from ddtrace.internal.compat import to_unicode +from ddtrace.vendor import wrapt + +from . import constants +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import ERROR_MSG +from ...constants import ERROR_TYPE +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from .utils import set_grpc_method_meta + + +def create_server_interceptor(pin): + def interceptor_function(continuation, handler_call_details): + if not pin.enabled: + return continuation(handler_call_details) + + rpc_method_handler = continuation(handler_call_details) + + # continuation returns an RpcMethodHandler instance if the RPC is + # considered serviced, or None otherwise + # https://grpc.github.io/grpc/python/grpc.html#grpc.ServerInterceptor.intercept_service + + if rpc_method_handler: + return _TracedRpcMethodHandler(pin, handler_call_details, rpc_method_handler) + + return rpc_method_handler + + return _ServerInterceptor(interceptor_function) + + +def _handle_server_exception(server_context, span): + if server_context is not None and hasattr(server_context, "_state") and server_context._state is not None: + code = to_unicode(server_context._state.code) + details = to_unicode(server_context._state.details) + span.error = 1 + span._set_str_tag(ERROR_MSG, details) + span._set_str_tag(ERROR_TYPE, code) + + +def _wrap_response_iterator(response_iterator, server_context, span): + try: + for response in response_iterator: + yield response + except Exception: + span.set_traceback() + _handle_server_exception(server_context, span) + raise + finally: + span.finish() + + +class _TracedRpcMethodHandler(wrapt.ObjectProxy): + def __init__(self, pin, handler_call_details, wrapped): + super(_TracedRpcMethodHandler, self).__init__(wrapped) + self._pin = pin + self._handler_call_details = handler_call_details + + def _fn(self, method_kind, behavior, args, kwargs): + tracer = self._pin.tracer + headers = dict(self._handler_call_details.invocation_metadata) + + trace_utils.activate_distributed_headers(tracer, int_config=config.grpc_server, request_headers=headers) + + span = tracer.trace( + "grpc", + span_type=SpanTypes.GRPC, + service=trace_utils.int_service(self._pin, config.grpc_server), + resource=self._handler_call_details.method, + ) + span.set_tag(SPAN_MEASURED_KEY) + + set_grpc_method_meta(span, self._handler_call_details.method, method_kind) + span._set_str_tag(constants.GRPC_SPAN_KIND_KEY, constants.GRPC_SPAN_KIND_VALUE_SERVER) + + sample_rate = config.grpc_server.get_analytics_sample_rate() + if sample_rate is not None: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate) + + # access server context by taking second argument as server context + # if not found, skip using context to tag span with server state information + server_context = args[1] if isinstance(args[1], grpc.ServicerContext) else None + + if self._pin.tags: + span.set_tags(self._pin.tags) + + try: + response_or_iterator = behavior(*args, **kwargs) + + if self.__wrapped__.response_streaming: + response_or_iterator = _wrap_response_iterator(response_or_iterator, server_context, span) + except Exception: + span.set_traceback() + _handle_server_exception(server_context, span) + raise + finally: + if not self.__wrapped__.response_streaming: + span.finish() + + return response_or_iterator + + def unary_unary(self, *args, **kwargs): + return self._fn(constants.GRPC_METHOD_KIND_UNARY, self.__wrapped__.unary_unary, args, kwargs) + + def unary_stream(self, *args, **kwargs): + return self._fn(constants.GRPC_METHOD_KIND_SERVER_STREAMING, self.__wrapped__.unary_stream, args, kwargs) + + def stream_unary(self, *args, **kwargs): + return self._fn(constants.GRPC_METHOD_KIND_CLIENT_STREAMING, self.__wrapped__.stream_unary, args, kwargs) + + def stream_stream(self, *args, **kwargs): + return self._fn(constants.GRPC_METHOD_KIND_BIDI_STREAMING, self.__wrapped__.stream_stream, args, kwargs) + + +class _ServerInterceptor(grpc.ServerInterceptor): + def __init__(self, interceptor_function): + self._fn = interceptor_function + + def intercept_service(self, continuation, handler_call_details): + return self._fn(continuation, handler_call_details) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/utils.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/utils.py new file mode 100644 index 000000000..c5bfa7062 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/grpc/utils.py @@ -0,0 +1,73 @@ +import logging + +from ddtrace.internal.compat import parse + +from . import constants + + +log = logging.getLogger(__name__) + + +def parse_method_path(method_path): + """Returns (package, service, method) tuple from parsing method path""" + # unpack method path based on "/{package}.{service}/{method}" + # first remove leading "/" as unnecessary + package_service, method_name = method_path.lstrip("/").rsplit("/", 1) + + # {package} is optional + package_service = package_service.rsplit(".", 1) + if len(package_service) == 2: + return package_service[0], package_service[1], method_name + + return None, package_service[0], method_name + + +def set_grpc_method_meta(span, method, method_kind): + method_path = method + method_package, method_service, method_name = parse_method_path(method_path) + if method_path is not None: + span._set_str_tag(constants.GRPC_METHOD_PATH_KEY, method_path) + if method_package is not None: + span._set_str_tag(constants.GRPC_METHOD_PACKAGE_KEY, method_package) + if method_service is not None: + span._set_str_tag(constants.GRPC_METHOD_SERVICE_KEY, method_service) + if method_name is not None: + span._set_str_tag(constants.GRPC_METHOD_NAME_KEY, method_name) + if method_kind is not None: + span._set_str_tag(constants.GRPC_METHOD_KIND_KEY, method_kind) + + +def set_grpc_client_meta(span, host, port): + if host: + span._set_str_tag(constants.GRPC_HOST_KEY, host) + if port: + span._set_str_tag(constants.GRPC_PORT_KEY, str(port)) + span._set_str_tag(constants.GRPC_SPAN_KIND_KEY, constants.GRPC_SPAN_KIND_VALUE_CLIENT) + + +def _parse_target_from_args(args, kwargs): + if "target" in kwargs: + target = kwargs["target"] + else: + target = args[0] + + try: + if target is None: + return + + # ensure URI follows RFC 3986 and is preceded by double slash + # https://tools.ietf.org/html/rfc3986#section-3.2 + parsed = parse.urlsplit("//" + target if not target.startswith("//") else target) + port = None + try: + port = parsed.port + except ValueError: + log.warning("Non-integer port in target '%s'", target) + + # an empty hostname in Python 2.7 will be an empty string rather than + # None + hostname = parsed.hostname if parsed.hostname is not None and len(parsed.hostname) > 0 else None + + return hostname, port + except ValueError: + log.warning("Malformed target '%s'.", target) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httplib/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httplib/__init__.py new file mode 100644 index 000000000..ce4fbeb53 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httplib/__init__.py @@ -0,0 +1,65 @@ +""" +Trace the standard library ``httplib``/``http.client`` libraries to trace +HTTP requests. + + +Enabling +~~~~~~~~ + +The httplib integration is disabled by default. It can be enabled when using +:ref:`ddtrace-run` using the ``DD_TRACE_HTTPLIB_ENABLED`` +environment variable:: + + DD_TRACE_HTTPLIB_ENABLED=true ddtrace-run .... + +The integration can also be enabled manually in code with +:ref:`patch_all()`:: + + from ddtrace import patch_all + patch_all(httplib=True) + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.httplib['distributed_tracing'] + + Include distributed tracing headers in requests sent from httplib. + + This option can also be set with the ``DD_HTTPLIB_DISTRIBUTED_TRACING`` + environment variable. + + Default: ``True`` + + +Instance Configuration +~~~~~~~~~~~~~~~~~~~~~~ + + +The integration can be configured per instance:: + + from ddtrace import config + + # Disable distributed tracing globally. + config.httplib['distributed_tracing'] = False + + # Change the service distributed tracing option only for this HTTP + # connection. + + # Python 2 + connection = urllib.HTTPConnection('www.datadog.com') + + # Python 3 + connection = http.client.HTTPConnection('www.datadog.com') + + cfg = config.get_from(connection) + cfg['distributed_tracing'] = True + + +:ref:`Headers tracing ` is supported for this integration. +""" +from .patch import patch +from .patch import unpatch + + +__all__ = ["patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httplib/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httplib/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..9a5a9114c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httplib/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httplib/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httplib/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..8a4e78a27 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httplib/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httplib/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httplib/patch.py new file mode 100644 index 000000000..cb32f3a0d --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httplib/patch.py @@ -0,0 +1,206 @@ +import sys + +import six + +from ddtrace import config +from ddtrace.vendor import wrapt + +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...ext import SpanTypes +from ...internal.compat import PY2 +from ...internal.compat import httplib +from ...internal.compat import parse +from ...internal.logger import get_logger +from ...pin import Pin +from ...propagation.http import HTTPPropagator +from ...utils.formats import asbool +from ...utils.formats import get_env +from ...utils.wrappers import unwrap as _u + + +span_name = "httplib.request" if PY2 else "http.client.request" + +log = get_logger(__name__) + + +config._add( + "httplib", + { + "distributed_tracing": asbool(get_env("httplib", "distributed_tracing", default=True)), + }, +) + + +def _wrap_init(func, instance, args, kwargs): + Pin(app="httplib", service=None, _config=config.httplib).onto(instance) + return func(*args, **kwargs) + + +def _wrap_getresponse(func, instance, args, kwargs): + # Use any attached tracer if available, otherwise use the global tracer + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + resp = None + try: + resp = func(*args, **kwargs) + return resp + finally: + try: + # Get the span attached to this instance, if available + span = getattr(instance, "_datadog_span", None) + if span: + if resp: + trace_utils.set_http_meta( + span, config.httplib, status_code=resp.status, response_headers=resp.getheaders() + ) + + span.finish() + delattr(instance, "_datadog_span") + except Exception: + log.debug("error applying request tags", exc_info=True) + + +def _wrap_request(func, instance, args, kwargs): + # Use any attached tracer if available, otherwise use the global tracer + pin = Pin.get_from(instance) + if should_skip_request(pin, instance): + return func(*args, **kwargs) + + cfg = config.get_from(instance) + + try: + # Create a new span and attach to this instance (so we can retrieve/update/close later on the response) + span = pin.tracer.trace(span_name, span_type=SpanTypes.HTTP) + setattr(instance, "_datadog_span", span) + + # propagate distributed tracing headers + if cfg.get("distributed_tracing"): + if len(args) > 3: + headers = args[3] + else: + headers = kwargs.setdefault("headers", {}) + HTTPPropagator.inject(span.context, headers) + except Exception: + log.debug("error configuring request", exc_info=True) + span = getattr(instance, "_datadog_span", None) + if span: + span.finish() + + try: + return func(*args, **kwargs) + except Exception: + span = getattr(instance, "_datadog_span", None) + exc_info = sys.exc_info() + if span: + span.set_exc_info(*exc_info) + span.finish() + six.reraise(*exc_info) + + +def _wrap_putrequest(func, instance, args, kwargs): + # Use any attached tracer if available, otherwise use the global tracer + pin = Pin.get_from(instance) + if should_skip_request(pin, instance): + return func(*args, **kwargs) + + try: + if hasattr(instance, "_datadog_span"): + # Reuse an existing span set in _wrap_request + span = instance._datadog_span + else: + # Create a new span and attach to this instance (so we can retrieve/update/close later on the response) + span = pin.tracer.trace(span_name, span_type=SpanTypes.HTTP) + setattr(instance, "_datadog_span", span) + + method, path = args[:2] + scheme = "https" if isinstance(instance, httplib.HTTPSConnection) else "http" + port = ":{port}".format(port=instance.port) + + if (scheme == "http" and instance.port == 80) or (scheme == "https" and instance.port == 443): + port = "" + url = "{scheme}://{host}{port}{path}".format(scheme=scheme, host=instance.host, port=port, path=path) + + # sanitize url + parsed = parse.urlparse(url) + sanitized_url = parse.urlunparse( + (parsed.scheme, parsed.netloc, parsed.path, parsed.params, None, parsed.fragment) # drop query + ) + trace_utils.set_http_meta(span, config.httplib, method=method, url=sanitized_url, query=parsed.query) + + # set analytics sample rate + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.httplib.get_analytics_sample_rate()) + except Exception: + log.debug("error applying request tags", exc_info=True) + + # Close the span to prevent memory leaks. + span = getattr(instance, "_datadog_span", None) + if span: + span.finish() + + try: + return func(*args, **kwargs) + except Exception: + span = getattr(instance, "_datadog_span", None) + exc_info = sys.exc_info() + if span: + span.set_exc_info(*exc_info) + span.finish() + six.reraise(*exc_info) + + +def _wrap_putheader(func, instance, args, kwargs): + span = getattr(instance, "_datadog_span", None) + if span: + request_headers = {args[0]: args[1]} + trace_utils.set_http_meta(span, config.httplib, request_headers=request_headers) + + return func(*args, **kwargs) + + +def should_skip_request(pin, request): + """Helper to determine if the provided request should be traced""" + if not pin or not pin.enabled(): + return True + + if hasattr(pin.tracer.writer, "agent_url"): + parsed = parse.urlparse(pin.tracer.writer.agent_url) + return request.host == parsed.hostname and request.port == parsed.port + return False + + +def patch(): + """patch the built-in urllib/httplib/httplib.client methods for tracing""" + if getattr(httplib, "__datadog_patch", False): + return + setattr(httplib, "__datadog_patch", True) + + # Patch the desired methods + setattr(httplib.HTTPConnection, "__init__", wrapt.FunctionWrapper(httplib.HTTPConnection.__init__, _wrap_init)) + setattr( + httplib.HTTPConnection, + "getresponse", + wrapt.FunctionWrapper(httplib.HTTPConnection.getresponse, _wrap_getresponse), + ) + setattr(httplib.HTTPConnection, "request", wrapt.FunctionWrapper(httplib.HTTPConnection.request, _wrap_request)) + setattr( + httplib.HTTPConnection, "putrequest", wrapt.FunctionWrapper(httplib.HTTPConnection.putrequest, _wrap_putrequest) + ) + setattr( + httplib.HTTPConnection, "putheader", wrapt.FunctionWrapper(httplib.HTTPConnection.putheader, _wrap_putheader) + ) + + +def unpatch(): + """unpatch any previously patched modules""" + if not getattr(httplib, "__datadog_patch", False): + return + setattr(httplib, "__datadog_patch", False) + + _u(httplib.HTTPConnection, "__init__") + _u(httplib.HTTPConnection, "getresponse") + _u(httplib.HTTPConnection, "request") + _u(httplib.HTTPConnection, "putrequest") + _u(httplib.HTTPConnection, "putheader") diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httpx/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httpx/__init__.py new file mode 100644 index 000000000..c98ca91b7 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httpx/__init__.py @@ -0,0 +1,93 @@ +""" +The httpx__ integration traces all HTTP requests made with the ``httpx`` +library. + +Enabling +~~~~~~~~ + +The ``httpx`` integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Alternatively, use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(httpx=True) + + # use httpx like usual + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.httpx['service'] + + The default service name for ``httpx`` requests. + By default the ``httpx`` integration will not define a service name and inherit + its service name from its parent span. + + If you are making calls to uninstrumented third party applications you can + set this setting, use the ``ddtrace.config.httpx['split_by_domain']`` setting, + or use a ``Pin`` to override an individual connection's settings (see example + below for ``Pin`` usage). + + This option can also be set with the ``DD_HTTPX_SERVICE`` environment + variable. + + Default: ``None`` + + +.. py:data:: ddtrace.config.httpx['distributed_tracing'] + + Whether or not to inject distributed tracing headers into requests. + + Default: ``True`` + + +.. py:data:: ddtrace.config.httpx['split_by_domain'] + + Whether or not to use the domain name of requests as the service name. This + setting can be overridden with session overrides (described in the Instance + Configuration section). + + This setting takes precedence over ``ddtrace.config.httpx['service']`` + + Default: ``False`` + + +Instance Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +To configure particular ``httpx`` client instances use the :ref:`Pin` API:: + + import httpx + from ddtrace import Pin + + client = httpx.Client() + # Override service name for this instance + Pin.override(client, service="custom-http-service") + + async_client = httpx.AsyncClient( + # Override service name for this instance + Pin.override(async_client, service="custom-async-http-service") + + +:ref:`Headers tracing ` is supported for this integration. + +:ref:`HTTP Tagging ` is supported for this integration. + +.. __: https://www.python-httpx.org/ +""" +from ...utils.importlib import require_modules + + +required_modules = ["httpx"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = [ + "patch", + "unpatch", + ] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httpx/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httpx/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..2b20158b8 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httpx/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httpx/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httpx/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..d090ef824 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httpx/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httpx/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httpx/patch.py new file mode 100644 index 000000000..6518fa974 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/httpx/patch.py @@ -0,0 +1,153 @@ +import typing + +import httpx +from six import ensure_binary +from six import ensure_text + +from ddtrace import config +from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY +from ddtrace.constants import SPAN_MEASURED_KEY +from ddtrace.contrib.trace_utils import distributed_tracing_enabled +from ddtrace.contrib.trace_utils import ext_service +from ddtrace.contrib.trace_utils import set_http_meta +from ddtrace.ext import SpanTypes +from ddtrace.pin import Pin +from ddtrace.propagation.http import HTTPPropagator +from ddtrace.utils import get_argument_value +from ddtrace.utils.formats import asbool +from ddtrace.utils.formats import get_env +from ddtrace.utils.wrappers import unwrap as _u +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + + +if typing.TYPE_CHECKING: + from ddtrace import Span + from ddtrace.vendor.wrapt import BoundFunctionWrapper + +config._add( + "httpx", + { + "distributed_tracing": asbool(get_env("httpx", "distributed_tracing", default=True)), + "split_by_domain": asbool(get_env("httpx", "split_by_domain", default=False)), + }, +) + + +def _url_to_str(url): + # type: (httpx.URL) -> str + """ + Helper to convert the httpx.URL parts from bytes to a str + """ + scheme, host, port, raw_path = url.raw + url = scheme + b"://" + host + if port is not None: + url += b":" + ensure_binary(str(port)) + url += raw_path + return ensure_text(url) + + +def _init_span(span, request): + # type: (Span, httpx.Request) -> None + if config.httpx.split_by_domain: + if hasattr(request.url, "netloc"): + span.service = request.url.netloc + else: + service = ensure_binary(request.url.host) + if request.url.port: + service += b":" + ensure_binary(str(request.url.port)) + span.service = service + + span.set_tag(SPAN_MEASURED_KEY) + + if distributed_tracing_enabled(config.httpx): + HTTPPropagator.inject(span.context, request.headers) + + sample_rate = config.httpx.get_analytics_sample_rate(use_global_config=True) + if sample_rate is not None: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate) + + +def _set_span_meta(span, request, response): + # type: (Span, httpx.Request, httpx.Response) -> None + set_http_meta( + span, + config.httpx, + method=request.method, + url=_url_to_str(request.url), + status_code=response.status_code if response else None, + query=request.url.query, + request_headers=request.headers, + response_headers=response.headers if response else None, + ) + + +async def _wrapped_async_send( + wrapped, # type: BoundFunctionWrapper + instance, # type: httpx.AsyncClient + args, # type: typing.Tuple[httpx.Request], + kwargs, # type: typing.Dict[typing.Str, typing.Any] +): + # type: (...) -> typing.Coroutine[None, None, httpx.Response] + req = get_argument_value(args, kwargs, 0, "request") + + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return await wrapped(*args, **kwargs) + + with pin.tracer.trace("http.request", service=ext_service(pin, config.httpx), span_type=SpanTypes.HTTP) as span: + _init_span(span, req) + resp = None + try: + resp = await wrapped(*args, **kwargs) + return resp + finally: + _set_span_meta(span, req, resp) + + +def _wrapped_sync_send( + wrapped, # type: BoundFunctionWrapper + instance, # type: httpx.AsyncClient + args, # type: typing.Tuple[httpx.Request] + kwargs, # type: typing.Dict[typing.Str, typing.Any] +): + # type: (...) -> httpx.Response + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + req = get_argument_value(args, kwargs, 0, "request") + + with pin.tracer.trace("http.request", service=ext_service(pin, config.httpx), span_type=SpanTypes.HTTP) as span: + _init_span(span, req) + resp = None + try: + resp = wrapped(*args, **kwargs) + return resp + finally: + _set_span_meta(span, req, resp) + + +def patch(): + # type: () -> None + if getattr(httpx, "_datadog_patch", False): + return + + setattr(httpx, "_datadog_patch", True) + + _w(httpx.AsyncClient, "send", _wrapped_async_send) + _w(httpx.Client, "send", _wrapped_sync_send) + + pin = Pin(app="httpx") + pin.onto(httpx.AsyncClient) + pin.onto(httpx.Client) + + +def unpatch(): + # type: () -> None + if not getattr(httpx, "_datadog_patch", False): + return + + setattr(httpx, "_datadog_patch", False) + + _u(httpx.AsyncClient, "send") + _u(httpx.Client, "send") diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/__init__.py new file mode 100644 index 000000000..e070711c2 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/__init__.py @@ -0,0 +1,43 @@ +""" +The ``jinja2`` integration traces templates loading, compilation and rendering. +Auto instrumentation is available using the ``patch``. The following is an example:: + + from ddtrace import patch + from jinja2 import Environment, FileSystemLoader + + patch(jinja2=True) + + env = Environment( + loader=FileSystemLoader("templates") + ) + template = env.get_template('mytemplate.html') + + +The library can be configured globally and per instance, using the Configuration API:: + + from ddtrace import config + + # Change service name globally + config.jinja2['service_name'] = 'jinja-templates' + + # change the service name only for this environment + cfg = config.get_from(env) + cfg['service_name'] = 'jinja-templates' + +By default, the service name is set to None, so it is inherited from the parent span. +If there is no parent span and the service name is not overridden the agent will drop the traces. +""" +from ...utils.importlib import require_modules + + +required_modules = ["jinja2"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = [ + "patch", + "unpatch", + ] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..228a95f5e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/__pycache__/constants.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/__pycache__/constants.cpython-38.pyc new file mode 100644 index 000000000..41dbe1e63 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/__pycache__/constants.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..50b8f7e45 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/constants.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/constants.py new file mode 100644 index 000000000..1bda6e1ca --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/constants.py @@ -0,0 +1 @@ +DEFAULT_TEMPLATE_NAME = "" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/patch.py new file mode 100644 index 000000000..7c1c098f8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/jinja2/patch.py @@ -0,0 +1,99 @@ +import jinja2 + +from ddtrace import config +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...pin import Pin +from ...utils import ArgumentError +from ...utils import get_argument_value +from ...utils.formats import get_env +from ...utils.wrappers import unwrap as _u +from .constants import DEFAULT_TEMPLATE_NAME + + +# default settings +config._add( + "jinja2", + { + "service_name": get_env("jinja2", "service_name"), + }, +) + + +def patch(): + if getattr(jinja2, "__datadog_patch", False): + # already patched + return + setattr(jinja2, "__datadog_patch", True) + Pin( + service=config.jinja2["service_name"], + _config=config.jinja2, + ).onto(jinja2.environment.Environment) + _w(jinja2, "environment.Template.render", _wrap_render) + _w(jinja2, "environment.Template.generate", _wrap_render) + _w(jinja2, "environment.Environment.compile", _wrap_compile) + _w(jinja2, "environment.Environment._load_template", _wrap_load_template) + + +def unpatch(): + if not getattr(jinja2, "__datadog_patch", False): + return + setattr(jinja2, "__datadog_patch", False) + _u(jinja2.Template, "render") + _u(jinja2.Template, "generate") + _u(jinja2.Environment, "compile") + _u(jinja2.Environment, "_load_template") + + +def _wrap_render(wrapped, instance, args, kwargs): + """Wrap `Template.render()` or `Template.generate()`""" + pin = Pin.get_from(instance.environment) + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + template_name = instance.name or DEFAULT_TEMPLATE_NAME + with pin.tracer.trace("jinja2.render", pin.service, span_type=SpanTypes.TEMPLATE) as span: + span.set_tag(SPAN_MEASURED_KEY) + try: + return wrapped(*args, **kwargs) + finally: + span.resource = template_name + span.set_tag("jinja2.template_name", template_name) + + +def _wrap_compile(wrapped, instance, args, kwargs): + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + try: + template_name = get_argument_value(args, kwargs, 1, "name") + except ArgumentError: + template_name = DEFAULT_TEMPLATE_NAME + + with pin.tracer.trace("jinja2.compile", pin.service, span_type=SpanTypes.TEMPLATE) as span: + try: + return wrapped(*args, **kwargs) + finally: + span.resource = template_name + span.set_tag("jinja2.template_name", template_name) + + +def _wrap_load_template(wrapped, instance, args, kwargs): + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + template_name = get_argument_value(args, kwargs, 0, "name") + with pin.tracer.trace("jinja2.load", pin.service, span_type=SpanTypes.TEMPLATE) as span: + template = None + try: + template = wrapped(*args, **kwargs) + return template + finally: + span.resource = template_name + span.set_tag("jinja2.template_name", template_name) + if template: + span.set_tag("jinja2.template_path", template.filename) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/__init__.py new file mode 100644 index 000000000..4e36a5f42 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/__init__.py @@ -0,0 +1,44 @@ +"""Instrument kombu to report AMQP messaging. + +``patch_all`` will not automatically patch your Kombu client to make it work, as this would conflict with the +Celery integration. You must specifically request kombu be patched, as in the example below. + +Note: To permit distributed tracing for the kombu integration you must enable the tracer with priority +sampling. Refer to the documentation here: +https://ddtrace.readthedocs.io/en/stable/advanced_usage.html#priority-sampling + +Without enabling distributed tracing, spans within a trace generated by the kombu integration might be dropped +without the whole trace being dropped. +:: + + from ddtrace import Pin, patch + import kombu + + # If not patched yet, you can patch kombu specifically + patch(kombu=True) + + # This will report a span with the default settings + conn = kombu.Connection("amqp://guest:guest@127.0.0.1:5672//") + conn.connect() + task_queue = kombu.Queue('tasks', kombu.Exchange('tasks'), routing_key='tasks') + to_publish = {'hello': 'world'} + producer = conn.Producer() + producer.publish(to_publish, + exchange=task_queue.exchange, + routing_key=task_queue.routing_key, + declare=[task_queue]) + + # Use a pin to specify metadata related to this client + Pin.override(producer, service='kombu-consumer') +""" + +from ...utils.importlib import require_modules + + +required_modules = ["kombu", "kombu.messaging"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + + __all__ = ["patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..599c2fe27 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/__pycache__/constants.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/__pycache__/constants.cpython-38.pyc new file mode 100644 index 000000000..c72844b69 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/__pycache__/constants.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..3434bfb50 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/__pycache__/utils.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/__pycache__/utils.cpython-38.pyc new file mode 100644 index 000000000..e22d8ec74 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/__pycache__/utils.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/constants.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/constants.py new file mode 100644 index 000000000..bcada46cd --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/constants.py @@ -0,0 +1 @@ +DEFAULT_SERVICE = "kombu" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/patch.py new file mode 100644 index 000000000..9fdd28df8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/patch.py @@ -0,0 +1,128 @@ +# 3p +import kombu + +from ddtrace import config +from ddtrace.vendor import wrapt + +# project +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import kombu as kombux +from ...pin import Pin +from ...propagation.http import HTTPPropagator +from ...utils import get_argument_value +from ...utils.formats import get_env +from ...utils.wrappers import unwrap +from .constants import DEFAULT_SERVICE +from .utils import HEADER_POS +from .utils import extract_conn_tags +from .utils import get_body_length_from_args +from .utils import get_exchange_from_args +from .utils import get_routing_key_from_args + + +# kombu default settings + +config._add( + "kombu", + { + "service_name": config.service or get_env("kombu", "service_name", default=DEFAULT_SERVICE), + }, +) + +propagator = HTTPPropagator + + +def patch(): + """Patch the instrumented methods + + This duplicated doesn't look nice. The nicer alternative is to use an ObjectProxy on top + of Kombu. However, it means that any "import kombu.Connection" won't be instrumented. + """ + if getattr(kombu, "_datadog_patch", False): + return + setattr(kombu, "_datadog_patch", True) + + _w = wrapt.wrap_function_wrapper + # We wrap the _publish method because the publish method: + # * defines defaults in its kwargs + # * potentially overrides kwargs with values from self + # * extracts/normalizes things like exchange + _w("kombu", "Producer._publish", traced_publish) + _w("kombu", "Consumer.receive", traced_receive) + + # We do not provide a service for producer spans since they represent + # external calls to another service. + # Instead the service should be inherited from the parent. + if config.service: + prod_service = None + # DEV: backwards-compatibility for users who set a kombu service + else: + prod_service = get_env("kombu", "service_name", default=DEFAULT_SERVICE) + + Pin( + service=prod_service, + app="kombu", + ).onto(kombu.messaging.Producer) + + Pin(service=config.kombu["service_name"], app="kombu").onto(kombu.messaging.Consumer) + + +def unpatch(): + if getattr(kombu, "_datadog_patch", False): + setattr(kombu, "_datadog_patch", False) + unwrap(kombu.Producer, "_publish") + unwrap(kombu.Consumer, "receive") + + +# +# tracing functions +# + + +def traced_receive(func, instance, args, kwargs): + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + # Signature only takes 2 args: (body, message) + message = get_argument_value(args, kwargs, 1, "message") + + trace_utils.activate_distributed_headers(pin.tracer, request_headers=message.headers, override=True) + + with pin.tracer.trace(kombux.RECEIVE_NAME, service=pin.service, span_type=SpanTypes.WORKER) as s: + s.set_tag(SPAN_MEASURED_KEY) + # run the command + exchange = message.delivery_info["exchange"] + s.resource = exchange + s.set_tag(kombux.EXCHANGE, exchange) + + s.set_tags(extract_conn_tags(message.channel.connection)) + s.set_tag(kombux.ROUTING_KEY, message.delivery_info["routing_key"]) + # set analytics sample rate + s.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.kombu.get_analytics_sample_rate()) + return func(*args, **kwargs) + + +def traced_publish(func, instance, args, kwargs): + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + with pin.tracer.trace(kombux.PUBLISH_NAME, service=pin.service, span_type=SpanTypes.WORKER) as s: + s.set_tag(SPAN_MEASURED_KEY) + exchange_name = get_exchange_from_args(args) + s.resource = exchange_name + s.set_tag(kombux.EXCHANGE, exchange_name) + if pin.tags: + s.set_tags(pin.tags) + s.set_tag(kombux.ROUTING_KEY, get_routing_key_from_args(args)) + s.set_tags(extract_conn_tags(instance.channel.connection)) + s.set_metric(kombux.BODY_LEN, get_body_length_from_args(args)) + # set analytics sample rate + s.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.kombu.get_analytics_sample_rate()) + # run the command + propagator.inject(s.context, args[HEADER_POS]) + return func(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/utils.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/utils.py new file mode 100644 index 000000000..fcce88a54 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/kombu/utils.py @@ -0,0 +1,49 @@ +""" +Some utils used by the dogtrace kombu integration +""" +from ...ext import kombu as kombux +from ...ext import net + + +PUBLISH_BODY_IDX = 0 +PUBLISH_ROUTING_KEY = 6 +PUBLISH_EXCHANGE_IDX = 9 + +HEADER_POS = 4 + + +def extract_conn_tags(connection): + """Transform kombu conn info into dogtrace metas""" + try: + host, port = connection.host.split(":") + return { + net.TARGET_HOST: host, + net.TARGET_PORT: port, + kombux.VHOST: connection.virtual_host, + } + except AttributeError: + # Unlikely that we don't have .host or .virtual_host but let's not die over it + return {} + + +def get_exchange_from_args(args): + """Extract the exchange + + The publish method extracts the name and hands that off to _publish (what we patch) + """ + + return args[PUBLISH_EXCHANGE_IDX] + + +def get_routing_key_from_args(args): + """Extract the routing key""" + + name = args[PUBLISH_ROUTING_KEY] + return name + + +def get_body_length_from_args(args): + """Extract the length of the body""" + + length = len(args[PUBLISH_BODY_IDX]) + return length diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/logging/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/logging/__init__.py new file mode 100644 index 000000000..a68e9d6c3 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/logging/__init__.py @@ -0,0 +1,76 @@ +""" +Datadog APM traces can be integrated with the logs product by: + +1. Having ``ddtrace`` patch the ``logging`` module. This will add trace +attributes to the log record. + +2. Updating the log formatter used by the application. In order to inject +tracing information into a log the formatter must be updated to include the +tracing attributes from the log record. ``ddtrace-run`` will do this +automatically for you by specifying a format. For more detail or instructions +for how to do this manually see the manual section below. + +With these in place the trace information will be injected into a log entry +which can be used to correlate the log and trace in Datadog. + + +ddtrace-run +----------- + +When using ``ddtrace-run``, enable patching by setting the environment variable +``DD_LOGS_INJECTION=true``. The logger by default will have a format that +includes trace information:: + + import logging + from ddtrace import tracer + + log = logging.getLogger() + log.level = logging.INFO + + + @tracer.wrap() + def hello(): + log.info('Hello, World!') + + hello() + +Manual Instrumentation +---------------------- + +If you prefer to instrument manually, patch the logging library then update the +log formatter as in the following example + +Make sure that your log format exactly matches the following:: + + from ddtrace import patch_all; patch_all(logging=True) + import logging + from ddtrace import tracer + + FORMAT = ('%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] ' + '[dd.service=%(dd.service)s dd.env=%(dd.env)s ' + 'dd.version=%(dd.version)s ' + 'dd.trace_id=%(dd.trace_id)s dd.span_id=%(dd.span_id)s]' + '- %(message)s') + logging.basicConfig(format=FORMAT) + log = logging.getLogger() + log.level = logging.INFO + + + @tracer.wrap() + def hello(): + log.info('Hello, World!') + + hello() +""" + +from ...utils.importlib import require_modules + + +required_modules = ["logging"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = ["patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/logging/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/logging/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..d8019528d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/logging/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/logging/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/logging/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..d5373fea9 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/logging/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/logging/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/logging/patch.py new file mode 100644 index 000000000..8ad2e9fd4 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/logging/patch.py @@ -0,0 +1,128 @@ +import logging + +import attr + +import ddtrace + +from ...utils import get_argument_value +from ...utils.wrappers import unwrap as _u +from ...vendor.wrapt import wrap_function_wrapper as _w + + +RECORD_ATTR_TRACE_ID = "dd.trace_id" +RECORD_ATTR_SPAN_ID = "dd.span_id" +RECORD_ATTR_ENV = "dd.env" +RECORD_ATTR_VERSION = "dd.version" +RECORD_ATTR_SERVICE = "dd.service" +RECORD_ATTR_VALUE_ZERO = "0" +RECORD_ATTR_VALUE_EMPTY = "" + +ddtrace.config._add( + "logging", + dict( + tracer=None, + ), +) # by default, override here for custom tracer + + +@attr.s(slots=True) +class DDLogRecord(object): + trace_id = attr.ib(type=int) + span_id = attr.ib(type=int) + service = attr.ib(type=str) + version = attr.ib(type=str) + env = attr.ib(type=str) + + +def _get_current_span(tracer=None): + """Helper to get the currently active span""" + if not tracer: + tracer = ddtrace.tracer + + # We might be calling this during library initialization, in which case `ddtrace.tracer` might + # be the `tracer` module and not the global tracer instance. + if not getattr(tracer, "enabled", False): + return None + + return tracer.current_span() + + +def _w_makeRecord(func, instance, args, kwargs): + # Get the LogRecord instance for this log + record = func(*args, **kwargs) + + setattr(record, RECORD_ATTR_VERSION, ddtrace.config.version or "") + setattr(record, RECORD_ATTR_ENV, ddtrace.config.env or "") + setattr(record, RECORD_ATTR_SERVICE, ddtrace.config.service or "") + + # logs from internal logger may explicitly pass the current span to + # avoid deadlocks in getting the current span while already in locked code. + span_from_log = getattr(record, ddtrace.constants.LOG_SPAN_KEY, None) + if isinstance(span_from_log, ddtrace.Span): + span = span_from_log + else: + span = _get_current_span(tracer=ddtrace.config.logging.tracer) + + if span: + setattr(record, RECORD_ATTR_TRACE_ID, str(span.trace_id)) + setattr(record, RECORD_ATTR_SPAN_ID, str(span.span_id)) + else: + setattr(record, RECORD_ATTR_TRACE_ID, RECORD_ATTR_VALUE_ZERO) + setattr(record, RECORD_ATTR_SPAN_ID, RECORD_ATTR_VALUE_ZERO) + + return record + + +def _w_StrFormatStyle_format(func, instance, args, kwargs): + # The format string "dd.service={dd.service}" expects + # the record to have a "dd" property which is an object that + # has a "service" property + # PercentStyle, and StringTemplateStyle both look for + # a "dd.service" property on the record + record = get_argument_value(args, kwargs, 0, "record") + + record.dd = DDLogRecord( + trace_id=getattr(record, RECORD_ATTR_TRACE_ID, RECORD_ATTR_VALUE_ZERO), + span_id=getattr(record, RECORD_ATTR_SPAN_ID, RECORD_ATTR_VALUE_ZERO), + service=getattr(record, RECORD_ATTR_SERVICE, ""), + version=getattr(record, RECORD_ATTR_VERSION, ""), + env=getattr(record, RECORD_ATTR_ENV, ""), + ) + + try: + return func(*args, **kwargs) + finally: + # We need to remove this extra attribute so it does not pollute other formatters + # For example: if we format with StrFormatStyle and then a JSON logger + # then the JSON logger will have `dd.{service,version,env,trace_id,span_id}` as + # well as the `record.dd` `DDLogRecord` instance + del record.dd + + +def patch(): + """ + Patch ``logging`` module in the Python Standard Library for injection of + tracer information by wrapping the base factory method ``Logger.makeRecord`` + """ + if getattr(logging, "_datadog_patch", False): + return + setattr(logging, "_datadog_patch", True) + + _w(logging.Logger, "makeRecord", _w_makeRecord) + if hasattr(logging, "StrFormatStyle"): + if hasattr(logging.StrFormatStyle, "_format"): + _w(logging.StrFormatStyle, "_format", _w_StrFormatStyle_format) + else: + _w(logging.StrFormatStyle, "format", _w_StrFormatStyle_format) + + +def unpatch(): + if getattr(logging, "_datadog_patch", False): + setattr(logging, "_datadog_patch", False) + + _u(logging.Logger, "makeRecord") + if hasattr(logging, "StrFormatStyle"): + if hasattr(logging.StrFormatStyle, "_format"): + _u(logging.StrFormatStyle, "_format") + else: + _u(logging.StrFormatStyle, "format") diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/__init__.py new file mode 100644 index 000000000..3294561ae --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/__init__.py @@ -0,0 +1,26 @@ +""" +The ``mako`` integration traces templates rendering. +Auto instrumentation is available using the ``patch``. The following is an example:: + + from ddtrace import patch + from mako.template import Template + + patch(mako=True) + + t = Template(filename="index.html") + +""" +from ...utils.importlib import require_modules + + +required_modules = ["mako"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = [ + "patch", + "unpatch", + ] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..01028822f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/__pycache__/constants.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/__pycache__/constants.cpython-38.pyc new file mode 100644 index 000000000..ba4a2b4a7 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/__pycache__/constants.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..36f582824 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/constants.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/constants.py new file mode 100644 index 000000000..1bda6e1ca --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/constants.py @@ -0,0 +1 @@ +DEFAULT_TEMPLATE_NAME = "" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/patch.py new file mode 100644 index 000000000..924c6881e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mako/patch.py @@ -0,0 +1,59 @@ +import mako +from mako.template import DefTemplate +from mako.template import Template + +from ddtrace import config + +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...pin import Pin +from ...utils.importlib import func_name +from ...utils.wrappers import unwrap as _u +from ...vendor.wrapt import wrap_function_wrapper as _w +from .constants import DEFAULT_TEMPLATE_NAME + + +def patch(): + if getattr(mako, "__datadog_patch", False): + # already patched + return + setattr(mako, "__datadog_patch", True) + + Pin(service=config.service or "mako", app="mako").onto(Template) + + _w(mako, "template.Template.render", _wrap_render) + _w(mako, "template.Template.render_unicode", _wrap_render) + _w(mako, "template.Template.render_context", _wrap_render) + + +def unpatch(): + if not getattr(mako, "__datadog_patch", False): + return + setattr(mako, "__datadog_patch", False) + + _u(mako.template.Template, "render") + _u(mako.template.Template, "render_unicode") + _u(mako.template.Template, "render_context") + + +def _wrap_render(wrapped, instance, args, kwargs): + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + # Determine the resource and `mako.template_name` tag value + # DefTemplate is a wrapper around a callable from another template, it does not have a filename + # https://github.com/sqlalchemy/mako/blob/c2c690ac9add584f2216dc655cdf8215b24ef03c/mako/template.py#L603-L622 + if isinstance(instance, DefTemplate) and hasattr(instance, "callable_"): + template_name = func_name(instance.callable_) + else: + template_name = getattr(instance, "filename", None) + template_name = template_name or DEFAULT_TEMPLATE_NAME + + with pin.tracer.trace(func_name(wrapped), pin.service, span_type=SpanTypes.TEMPLATE) as span: + span.set_tag(SPAN_MEASURED_KEY) + try: + return wrapped(*args, **kwargs) + finally: + span.resource = template_name + span.set_tag("mako.template_name", template_name) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mariadb/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mariadb/__init__.py new file mode 100644 index 000000000..f1803653d --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mariadb/__init__.py @@ -0,0 +1,65 @@ +""" +The MariaDB integration instruments the +`MariaDB library `_ to trace queries. + + +Enabling +~~~~~~~~ + +The MariaDB integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(mariadb=True) + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.mariadb["service"] + + The service name reported by default for MariaDB spans. + + This option can also be set with the ``DD_MARIADB_SERVICE`` environment + variable. + + Default: ``"mariadb"`` + + +Instance Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +To configure the mariadb integration on an per-connection basis use the +``Pin`` API:: + + from ddtrace import Pin + from ddtrace import patch + + # Make sure to patch before importing mariadb + patch(mariadb=True) + + import mariadb.connector + + # This will report a span with the default settings + conn = mariadb.connector.connect(user="alice", password="b0b", host="localhost", port=3306, database="test") + + # Use a pin to override the service name for this connection. + Pin.override(conn, service="mariadb-users") + + cursor = conn.cursor() + cursor.execute("SELECT 6*7 AS the_answer;") + +""" +from ...utils.importlib import require_modules + + +required_modules = ["mariadb"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = ["patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mariadb/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mariadb/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..ad9f06bea Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mariadb/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mariadb/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mariadb/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..2e49f9b4f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mariadb/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mariadb/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mariadb/patch.py new file mode 100644 index 000000000..4b90cc807 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mariadb/patch.py @@ -0,0 +1,49 @@ +import mariadb + +from ddtrace import Pin +from ddtrace import config +from ddtrace.contrib.dbapi import TracedConnection +from ddtrace.ext import db +from ddtrace.ext import net +from ddtrace.utils.formats import asbool +from ddtrace.utils.formats import get_env +from ddtrace.utils.wrappers import unwrap +from ddtrace.vendor import wrapt + + +config._add( + "mariadb", + dict( + trace_fetch_methods=asbool(get_env("mariadb", "trace_fetch_methods", default=False)), + _default_service="mariadb", + ), +) + + +def patch(): + if getattr(mariadb, "_datadog_patch", False): + return + setattr(mariadb, "_datadog_patch", True) + wrapt.wrap_function_wrapper("mariadb", "connect", _connect) + + +def unpatch(): + if getattr(mariadb, "_datadog_patch", False): + setattr(mariadb, "_datadog_patch", False) + unwrap(mariadb, "connect") + + +def _connect(func, instance, args, kwargs): + conn = func(*args, **kwargs) + tags = { + net.TARGET_HOST: kwargs["host"], + net.TARGET_PORT: kwargs["port"], + db.USER: kwargs["user"], + db.NAME: kwargs["database"], + } + + pin = Pin(app="mariadb", tags=tags) + + wrapped = TracedConnection(conn, pin=pin, cfg=config.mariadb) + pin.onto(wrapped) + return wrapped diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/__init__.py new file mode 100644 index 000000000..7a2b65665 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/__init__.py @@ -0,0 +1,49 @@ +""" +The molten web framework is automatically traced by ``ddtrace`` when calling ``patch``:: + + from molten import App, Route + from ddtrace import patch_all; patch_all(molten=True) + + def hello(name: str, age: int) -> str: + return f'Hello {age} year old named {name}!' + app = App(routes=[Route('/hello/{name}/{age}', hello)]) + + +You may also enable molten tracing automatically via ``ddtrace-run``:: + + ddtrace-run python app.py + + +Configuration +~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.molten['distributed_tracing'] + + Whether to parse distributed tracing headers from requests received by your Molten app. + + Default: ``True`` + +.. py:data:: ddtrace.config.molten['service_name'] + + The service name reported for your Molten app. + + Can also be configured via the ``DD_SERVICE`` or ``DD_MOLTEN_SERVICE_NAME`` environment variables. + + Default: ``'molten'`` + +:ref:`All HTTP tags ` are supported for this integration. + +""" +from ...utils.importlib import require_modules + + +required_modules = ["molten"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from . import patch as _patch + + patch = _patch.patch + unpatch = _patch.unpatch + + __all__ = ["patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..ca4037794 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..7e972a68a Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/__pycache__/wrappers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/__pycache__/wrappers.cpython-38.pyc new file mode 100644 index 000000000..b5e9fd935 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/__pycache__/wrappers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/patch.py new file mode 100644 index 000000000..3e13c7493 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/patch.py @@ -0,0 +1,164 @@ +import molten + +from ddtrace.vendor import wrapt +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from .. import trace_utils +from ... import Pin +from ... import config +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...internal.compat import urlencode +from ...utils.formats import asbool +from ...utils.formats import get_env +from ...utils.importlib import func_name +from ...utils.version import parse_version +from ...utils.wrappers import unwrap as _u +from .wrappers import MOLTEN_ROUTE +from .wrappers import WrapperComponent +from .wrappers import WrapperMiddleware +from .wrappers import WrapperRenderer +from .wrappers import WrapperRouter + + +MOLTEN_VERSION = parse_version(molten.__version__) + +# Configure default configuration +config._add( + "molten", + dict( + _default_service="molten", + app="molten", + distributed_tracing=asbool(get_env("molten", "distributed_tracing", default=True)), + ), +) + + +def patch(): + """Patch the instrumented methods""" + if getattr(molten, "_datadog_patch", False): + return + setattr(molten, "_datadog_patch", True) + + pin = Pin(app=config.molten["app"]) + + # add pin to module since many classes use __slots__ + pin.onto(molten) + + _w(molten.BaseApp, "__init__", patch_app_init) + _w(molten.App, "__call__", patch_app_call) + + +def unpatch(): + """Remove instrumentation""" + if getattr(molten, "_datadog_patch", False): + setattr(molten, "_datadog_patch", False) + + # remove pin + pin = Pin.get_from(molten) + if pin: + pin.remove_from(molten) + + _u(molten.BaseApp, "__init__") + _u(molten.App, "__call__") + + +def patch_app_call(wrapped, instance, args, kwargs): + """Patch wsgi interface for app""" + pin = Pin.get_from(molten) + + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + # DEV: This is safe because this is the args for a WSGI handler + # https://www.python.org/dev/peps/pep-3333/ + environ, start_response = args + + request = molten.http.Request.from_environ(environ) + resource = func_name(wrapped) + + # request.headers is type Iterable[Tuple[str, str]] + trace_utils.activate_distributed_headers( + pin.tracer, int_config=config.molten, request_headers=dict(request.headers) + ) + + with pin.tracer.trace( + "molten.request", + service=trace_utils.int_service(pin, config.molten), + resource=resource, + span_type=SpanTypes.WEB, + ) as span: + span.set_tag(SPAN_MEASURED_KEY) + # set analytics sample rate with global config enabled + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.molten.get_analytics_sample_rate(use_global_config=True)) + + @wrapt.function_wrapper + def _w_start_response(wrapped, instance, args, kwargs): + """Patch respond handling to set metadata""" + + pin = Pin.get_from(molten) + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + status, headers, exc_info = args + code, _, _ = status.partition(" ") + + try: + code = int(code) + except ValueError: + pass + + if not span.get_tag(MOLTEN_ROUTE): + # if route never resolve, update root resource + span.resource = u"{} {}".format(request.method, code) + + trace_utils.set_http_meta(span, config.molten, status_code=code) + + return wrapped(*args, **kwargs) + + # patching for extracting response code + start_response = _w_start_response(start_response) + + url = "%s://%s:%s%s" % ( + request.scheme, + request.host, + request.port, + request.path, + ) + query = urlencode(dict(request.params)) + trace_utils.set_http_meta( + span, config.molten, method=request.method, url=url, query=query, request_headers=request.headers + ) + + span.set_tag("molten.version", molten.__version__) + return wrapped(environ, start_response, **kwargs) + + +def patch_app_init(wrapped, instance, args, kwargs): + """Patch app initialization of middleware, components and renderers""" + # allow instance to be initialized before wrapping them + wrapped(*args, **kwargs) + + # add Pin to instance + pin = Pin.get_from(molten) + + if not pin or not pin.enabled(): + return + + # Wrappers here allow us to trace objects without altering class or instance + # attributes, which presents a problem when classes in molten use + # ``__slots__`` + + instance.router = WrapperRouter(instance.router) + + # wrap middleware functions/callables + instance.middleware = [WrapperMiddleware(mw) for mw in instance.middleware] + + # wrap components objects within injector + # NOTE: the app instance also contains a list of components but it does not + # appear to be used for anything passing along to the dependency injector + instance.injector.components = [WrapperComponent(c) for c in instance.injector.components] + + # but renderers objects + instance.renderers = [WrapperRenderer(r) for r in instance.renderers] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/wrappers.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/wrappers.py new file mode 100644 index 000000000..b2068858e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/molten/wrappers.py @@ -0,0 +1,105 @@ +import molten + +from ddtrace import config +from ddtrace.vendor import wrapt + +from .. import trace_utils +from ... import Pin +from ...utils.importlib import func_name + + +MOLTEN_ROUTE = "molten.route" + + +def trace_wrapped(resource, wrapped, *args, **kwargs): + pin = Pin.get_from(molten) + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + with pin.tracer.trace(func_name(wrapped), service=trace_utils.int_service(pin, config.molten), resource=resource): + return wrapped(*args, **kwargs) + + +def trace_func(resource): + """Trace calls to function using provided resource name""" + + @wrapt.function_wrapper + def _trace_func(wrapped, instance, args, kwargs): + pin = Pin.get_from(molten) + + if not pin or not pin.enabled(): + return wrapped(*args, **kwargs) + + with pin.tracer.trace( + func_name(wrapped), service=trace_utils.int_service(pin, config.molten, pin), resource=resource + ): + return wrapped(*args, **kwargs) + + return _trace_func + + +class WrapperComponent(wrapt.ObjectProxy): + """Tracing of components""" + + def can_handle_parameter(self, *args, **kwargs): + func = self.__wrapped__.can_handle_parameter + cname = func_name(self.__wrapped__) + resource = "{}.{}".format(cname, func.__name__) + return trace_wrapped(resource, func, *args, **kwargs) + + # TODO[tahir]: the signature of a wrapped resolve method causes DIError to + # be thrown since parameter types cannot be determined + + +class WrapperRenderer(wrapt.ObjectProxy): + """Tracing of renderers""" + + def render(self, *args, **kwargs): + func = self.__wrapped__.render + cname = func_name(self.__wrapped__) + resource = "{}.{}".format(cname, func.__name__) + return trace_wrapped(resource, func, *args, **kwargs) + + +class WrapperMiddleware(wrapt.ObjectProxy): + """Tracing of callable functional-middleware""" + + def __call__(self, *args, **kwargs): + func = self.__wrapped__.__call__ + resource = func_name(self.__wrapped__) + return trace_wrapped(resource, func, *args, **kwargs) + + +class WrapperRouter(wrapt.ObjectProxy): + """Tracing of router on the way back from a matched route""" + + def match(self, *args, **kwargs): + # catch matched route and wrap tracer around its handler and set root span resource + func = self.__wrapped__.match + route_and_params = func(*args, **kwargs) + + pin = Pin.get_from(molten) + if not pin or not pin.enabled(): + return route_and_params + + if route_and_params is not None: + route, params = route_and_params + + route.handler = trace_func(func_name(route.handler))(route.handler) + + # update root span resource while we know the matched route + resource = "{} {}".format( + route.method, + route.template, + ) + root_span = pin.tracer.current_root_span() + root_span.resource = resource + + # if no root route set make sure we record it based on this resolved + # route + if root_span and not root_span.get_tag(MOLTEN_ROUTE): + root_span.set_tag(MOLTEN_ROUTE, route.name) + + return route, params + + return route_and_params diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/__init__.py new file mode 100644 index 000000000..cd76b8c1a --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/__init__.py @@ -0,0 +1,30 @@ +"""Instrument mongoengine to report MongoDB queries. + +``patch_all`` will automatically patch your mongoengine connect method to make it work. +:: + + from ddtrace import Pin, patch + import mongoengine + + # If not patched yet, you can patch mongoengine specifically + patch(mongoengine=True) + + # At that point, mongoengine is instrumented with the default settings + mongoengine.connect('db', alias='default') + + # Use a pin to specify metadata related to this client + client = mongoengine.connect('db', alias='master') + Pin.override(client, service="mongo-master") +""" + +from ...utils.importlib import require_modules + + +required_modules = ["mongoengine"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import trace_mongoengine + + __all__ = ["patch", "trace_mongoengine"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..6a4ff442c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..cc520b156 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/__pycache__/trace.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/__pycache__/trace.cpython-38.pyc new file mode 100644 index 000000000..1b35489c9 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/__pycache__/trace.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/patch.py new file mode 100644 index 000000000..4d0b25acf --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/patch.py @@ -0,0 +1,21 @@ +import mongoengine + +from ...utils.deprecation import deprecated +from .trace import WrappedConnect + + +# Original connect function +_connect = mongoengine.connect + + +def patch(): + setattr(mongoengine, "connect", WrappedConnect(_connect)) + + +def unpatch(): + setattr(mongoengine, "connect", _connect) + + +@deprecated(message="Use patching instead (see the docs).", version="1.0.0") +def trace_mongoengine(*args, **kwargs): + return _connect diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/trace.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/trace.py new file mode 100644 index 000000000..3deeddef0 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mongoengine/trace.py @@ -0,0 +1,30 @@ +# 3p +# project +import ddtrace +from ddtrace.contrib.pymongo.client import TracedMongoClient +from ddtrace.ext import mongo as mongox +from ddtrace.vendor import wrapt + + +# TODO(Benjamin): we should instrument register_connection instead, because more generic +# We should also extract the "alias" attribute and set it as a meta +class WrappedConnect(wrapt.ObjectProxy): + """WrappedConnect wraps mongoengines 'connect' function to ensure + that all returned connections are wrapped for tracing. + """ + + def __init__(self, connect): + super(WrappedConnect, self).__init__(connect) + ddtrace.Pin(service=mongox.SERVICE, tracer=ddtrace.tracer).onto(self) + + def __call__(self, *args, **kwargs): + client = self.__wrapped__(*args, **kwargs) + pin = ddtrace.Pin.get_from(self) + if pin: + # mongoengine uses pymongo internally, so we can just piggyback on the + # existing pymongo integration and make sure that the connections it + # uses internally are traced. + client = TracedMongoClient(client) + ddtrace.Pin(service=pin.service, tracer=pin.tracer).onto(client) + + return client diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/__init__.py new file mode 100644 index 000000000..cefff53c6 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/__init__.py @@ -0,0 +1,76 @@ +""" +The mysql integration instruments the mysql library to trace MySQL queries. + + +Enabling +~~~~~~~~ + +The mysql integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(mysql=True) + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.mysql["service"] + + The service name reported by default for mysql spans. + + This option can also be set with the ``DD_MYSQL_SERVICE`` environment + variable. + + Default: ``"mysql"`` + +.. py:data:: ddtrace.config.mysql["trace_fetch_methods"] + + Whether or not to trace fetch methods. + + Can also configured via the ``DD_MYSQL_TRACE_FETCH_METHODS`` environment variable. + + Default: ``False`` + + +Instance Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +To configure the mysql integration on an per-connection basis use the +``Pin`` API:: + + from ddtrace import Pin + # Make sure to import mysql.connector and not the 'connect' function, + # otherwise you won't have access to the patched version + import mysql.connector + + # This will report a span with the default settings + conn = mysql.connector.connect(user="alice", password="b0b", host="localhost", port=3306, database="test") + + # Use a pin to override the service name for this connection. + Pin.override(conn, service='mysql-users') + + cursor = conn.cursor() + cursor.execute("SELECT 6*7 AS the_answer;") + + +Only the default full-Python integration works. The binary C connector, +provided by _mysql_connector, is not supported. + +Help on mysql.connector can be found on: +https://dev.mysql.com/doc/connector-python/en/ +""" +from ...utils.importlib import require_modules + + +# check `mysql-connector` availability +required_modules = ["mysql.connector"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .tracers import get_traced_mysql_connection + + __all__ = ["get_traced_mysql_connection", "patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..fedd2fdc1 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..203620e19 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/__pycache__/tracers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/__pycache__/tracers.cpython-38.pyc new file mode 100644 index 000000000..044274c97 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/__pycache__/tracers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/patch.py new file mode 100644 index 000000000..c26a3771e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/patch.py @@ -0,0 +1,57 @@ +import mysql.connector + +from ddtrace import Pin +from ddtrace import config +from ddtrace.contrib.dbapi import TracedConnection +from ddtrace.vendor import wrapt + +from ...ext import db +from ...ext import net +from ...utils.formats import asbool +from ...utils.formats import get_env + + +config._add( + "mysql", + dict( + _default_service="mysql", + trace_fetch_methods=asbool(get_env("mysql", "trace_fetch_methods", default=False)), + ), +) + +CONN_ATTR_BY_TAG = { + net.TARGET_HOST: "server_host", + net.TARGET_PORT: "server_port", + db.USER: "user", + db.NAME: "database", +} + + +def patch(): + wrapt.wrap_function_wrapper("mysql.connector", "connect", _connect) + # `Connect` is an alias for `connect`, patch it too + if hasattr(mysql.connector, "Connect"): + mysql.connector.Connect = mysql.connector.connect + + +def unpatch(): + if isinstance(mysql.connector.connect, wrapt.ObjectProxy): + mysql.connector.connect = mysql.connector.connect.__wrapped__ + if hasattr(mysql.connector, "Connect"): + mysql.connector.Connect = mysql.connector.connect + + +def _connect(func, instance, args, kwargs): + conn = func(*args, **kwargs) + return patch_conn(conn) + + +def patch_conn(conn): + + tags = {t: getattr(conn, a) for t, a in CONN_ATTR_BY_TAG.items() if getattr(conn, a, "") != ""} + pin = Pin(app="mysql", tags=tags) + + # grab the metadata from the conn + wrapped = TracedConnection(conn, pin=pin, cfg=config.mysql) + pin.onto(wrapped) + return wrapped diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/tracers.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/tracers.py new file mode 100644 index 000000000..439c0d867 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysql/tracers.py @@ -0,0 +1,8 @@ +import mysql.connector + +from ...utils.deprecation import deprecated + + +@deprecated(message="Use patching instead (see the docs).", version="1.0.0") +def get_traced_mysql_connection(*args, **kwargs): + return mysql.connector.MySQLConnection diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysqldb/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysqldb/__init__.py new file mode 100644 index 000000000..3b555c13c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysqldb/__init__.py @@ -0,0 +1,74 @@ +"""The mysqldb integration instruments the mysqlclient library to trace MySQL queries. + + +Enabling +~~~~~~~~ + +The integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(mysqldb=True) + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.mysqldb["service"] + + The service name reported by default for spans. + + This option can also be set with the ``DD_MYSQLDB_SERVICE`` environment + variable. + + Default: ``"mysql"`` + +.. py:data:: ddtrace.config.mysqldb["trace_fetch_methods"] + + Whether or not to trace fetch methods. + + Can also configured via the ``DD_MYSQLDB_TRACE_FETCH_METHODS`` environment variable. + + Default: ``False`` + + +Instance Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +To configure the integration on an per-connection basis use the +``Pin`` API:: + + # Make sure to import MySQLdb and not the 'connect' function, + # otherwise you won't have access to the patched version + from ddtrace import Pin + import MySQLdb + + # This will report a span with the default settings + conn = MySQLdb.connect(user="alice", passwd="b0b", host="localhost", port=3306, db="test") + + # Use a pin to override the service. + Pin.override(conn, service='mysql-users') + + cursor = conn.cursor() + cursor.execute("SELECT 6*7 AS the_answer;") + + +This package works for mysqlclient. Only the default full-Python integration works. The binary C connector provided by +_mysql is not supported. + +Help on mysqlclient can be found on: +https://mysqlclient.readthedocs.io/ + +""" +from ...utils.importlib import require_modules + + +required_modules = ["MySQLdb"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + + __all__ = ["patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysqldb/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysqldb/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..c47b3ddf8 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysqldb/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysqldb/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysqldb/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..335a59852 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysqldb/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysqldb/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysqldb/patch.py new file mode 100644 index 000000000..e08f46547 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/mysqldb/patch.py @@ -0,0 +1,75 @@ +# 3p +import MySQLdb + +# project +from ddtrace import Pin +from ddtrace import config +from ddtrace.contrib.dbapi import TracedConnection +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from ...ext import db +from ...ext import net +from ...utils.formats import asbool +from ...utils.formats import get_env +from ...utils.wrappers import unwrap as _u + + +config._add( + "mysqldb", + dict( + _default_service="mysql", + trace_fetch_methods=asbool(get_env("mysqldb", "trace_fetch_methods", default=False)), + ), +) + +KWPOS_BY_TAG = { + net.TARGET_HOST: ("host", 0), + db.USER: ("user", 1), + db.NAME: ("db", 3), +} + + +def patch(): + # patch only once + if getattr(MySQLdb, "__datadog_patch", False): + return + setattr(MySQLdb, "__datadog_patch", True) + + # `Connection` and `connect` are aliases for + # `Connect`; patch them too + _w("MySQLdb", "Connect", _connect) + if hasattr(MySQLdb, "Connection"): + _w("MySQLdb", "Connection", _connect) + if hasattr(MySQLdb, "connect"): + _w("MySQLdb", "connect", _connect) + + +def unpatch(): + if not getattr(MySQLdb, "__datadog_patch", False): + return + setattr(MySQLdb, "__datadog_patch", False) + + # unpatch MySQLdb + _u(MySQLdb, "Connect") + if hasattr(MySQLdb, "Connection"): + _u(MySQLdb, "Connection") + if hasattr(MySQLdb, "connect"): + _u(MySQLdb, "connect") + + +def _connect(func, instance, args, kwargs): + conn = func(*args, **kwargs) + return patch_conn(conn, *args, **kwargs) + + +def patch_conn(conn, *args, **kwargs): + tags = { + t: kwargs[k] if k in kwargs else args[p] for t, (k, p) in KWPOS_BY_TAG.items() if k in kwargs or len(args) > p + } + tags[net.TARGET_PORT] = conn.port + pin = Pin(app="mysql", tags=tags) + + # grab the metadata from the conn + wrapped = TracedConnection(conn, pin=pin, cfg=config.mysqldb) + pin.onto(wrapped) + return wrapped diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/__init__.py new file mode 100644 index 000000000..0b7ed90d1 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/__init__.py @@ -0,0 +1,65 @@ +""" +The psycopg integration instruments the psycopg2 library to trace Postgres queries. + + +Enabling +~~~~~~~~ + +The psycopg integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(psycopg=True) + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.psycopg["service"] + + The service name reported by default for psycopg spans. + + This option can also be set with the ``DD_PSYCOPG_SERVICE`` environment + variable. + + Default: ``"postgres"`` + +.. py:data:: ddtrace.config.psycopg["trace_fetch_methods"] + + Whether or not to trace fetch methods. + + Can also configured via the ``DD_PSYCOPG_TRACE_FETCH_METHODS`` environment variable. + + Default: ``False`` + + +Instance Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +To configure the psycopg integration on an per-connection basis use the +``Pin`` API:: + + from ddtrace import Pin + import psycopg2 + + db = psycopg2.connect(connection_factory=factory) + # Use a pin to override the service name. + Pin.override(db, service="postgres-users") + + cursor = db.cursor() + cursor.execute("select * from users where id = 1") +""" +from ...utils.importlib import require_modules + + +required_modules = ["psycopg2"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .connection import connection_factory + from .patch import patch + from .patch import patch_conn + + __all__ = ["connection_factory", "patch", "patch_conn"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..73dfb0ec2 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/__pycache__/connection.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/__pycache__/connection.cpython-38.pyc new file mode 100644 index 000000000..3b24d201f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/__pycache__/connection.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..83e447e78 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/connection.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/connection.py new file mode 100644 index 000000000..412fef9a8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/connection.py @@ -0,0 +1,97 @@ +""" +Tracing utilities for the psycopg potgres client library. +""" + +# stdlib +import functools + +# 3p +from psycopg2.extensions import connection +from psycopg2.extensions import cursor + +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import db +from ...ext import net +from ...ext import sql +from ...utils.deprecation import deprecated + + +@deprecated(message="Use patching instead (see the docs).", version="1.0.0") +def connection_factory(tracer, service="postgres"): + """Return a connection factory class that will can be used to trace + postgres queries. + + >>> factory = connection_factor(my_tracer, service='my_db_service') + >>> conn = pyscopg2.connect(..., connection_factory=factory) + """ + + return functools.partial( + TracedConnection, + datadog_tracer=tracer, + datadog_service=service, + ) + + +class TracedCursor(cursor): + """Wrapper around cursor creating one span per query""" + + def __init__(self, *args, **kwargs): + self._datadog_tracer = kwargs.pop("datadog_tracer", None) + self._datadog_service = kwargs.pop("datadog_service", None) + self._datadog_tags = kwargs.pop("datadog_tags", None) + super(TracedCursor, self).__init__(*args, **kwargs) + + def execute(self, query, vars=None): # noqa: A002 + """just wrap the cursor execution in a span""" + if not self._datadog_tracer: + return cursor.execute(self, query, vars) + + with self._datadog_tracer.trace("postgres.query", service=self._datadog_service, span_type=SpanTypes.SQL) as s: + s.set_tag(SPAN_MEASURED_KEY) + if not s.sampled: + return super(TracedCursor, self).execute(query, vars) + + s.resource = query + s.set_tags(self._datadog_tags) + try: + return super(TracedCursor, self).execute(query, vars) + finally: + s.set_metric("db.rowcount", self.rowcount) + + def callproc(self, procname, vars=None): # noqa: A002 + """just wrap the execution in a span""" + return cursor.callproc(self, procname, vars) + + +class TracedConnection(connection): + """Wrapper around psycopg2 for tracing""" + + def __init__(self, *args, **kwargs): + + self._datadog_tracer = kwargs.pop("datadog_tracer", None) + self._datadog_service = kwargs.pop("datadog_service", None) + + super(TracedConnection, self).__init__(*args, **kwargs) + + # add metadata (from the connection, string, etc) + dsn = sql.parse_pg_dsn(self.dsn) + self._datadog_tags = { + net.TARGET_HOST: dsn.get("host"), + net.TARGET_PORT: dsn.get("port"), + db.NAME: dsn.get("dbname"), + db.USER: dsn.get("user"), + "db.application": dsn.get("application_name"), + } + + self._datadog_cursor_class = functools.partial( + TracedCursor, + datadog_tracer=self._datadog_tracer, + datadog_service=self._datadog_service, + datadog_tags=self._datadog_tags, + ) + + def cursor(self, *args, **kwargs): + """register our custom cursor factory""" + kwargs.setdefault("cursor_factory", self._datadog_cursor_class) + return super(TracedConnection, self).cursor(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/patch.py new file mode 100644 index 000000000..b144f039b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/psycopg/patch.py @@ -0,0 +1,193 @@ +# 3p +import psycopg2 +from psycopg2.sql import Composable + +from ddtrace import Pin +from ddtrace import config +from ddtrace.contrib import dbapi +from ddtrace.ext import db +from ddtrace.ext import net +from ddtrace.ext import sql +from ddtrace.vendor import wrapt + +from ...utils.formats import asbool +from ...utils.formats import get_env +from ...utils.version import parse_version + + +config._add( + "psycopg", + dict( + _default_service="postgres", + trace_fetch_methods=asbool(get_env("psycopg", "trace_fetch_methods", default=False)), + ), +) + +# Original connect method +_connect = psycopg2.connect + +PSYCOPG2_VERSION = parse_version(psycopg2.__version__) + + +def patch(): + """Patch monkey patches psycopg's connection function + so that the connection's functions are traced. + """ + if getattr(psycopg2, "_datadog_patch", False): + return + setattr(psycopg2, "_datadog_patch", True) + + wrapt.wrap_function_wrapper(psycopg2, "connect", patched_connect) + _patch_extensions(_psycopg2_extensions) # do this early just in case + + +def unpatch(): + if getattr(psycopg2, "_datadog_patch", False): + setattr(psycopg2, "_datadog_patch", False) + psycopg2.connect = _connect + _unpatch_extensions(_psycopg2_extensions) + + +class Psycopg2TracedCursor(dbapi.TracedCursor): + """TracedCursor for psycopg2""" + + def _trace_method(self, method, name, resource, extra_tags, *args, **kwargs): + # treat psycopg2.sql.Composable resource objects as strings + if isinstance(resource, Composable): + resource = resource.as_string(self.__wrapped__) + + return super(Psycopg2TracedCursor, self)._trace_method(method, name, resource, extra_tags, *args, **kwargs) + + +class Psycopg2FetchTracedCursor(Psycopg2TracedCursor, dbapi.FetchTracedCursor): + """FetchTracedCursor for psycopg2""" + + +class Psycopg2TracedConnection(dbapi.TracedConnection): + """TracedConnection wraps a Connection with tracing code.""" + + def __init__(self, conn, pin=None, cursor_cls=None): + if not cursor_cls: + # Do not trace `fetch*` methods by default + cursor_cls = Psycopg2FetchTracedCursor if config.psycopg.trace_fetch_methods else Psycopg2TracedCursor + + super(Psycopg2TracedConnection, self).__init__(conn, pin, config.psycopg, cursor_cls=cursor_cls) + + +def patch_conn(conn, traced_conn_cls=Psycopg2TracedConnection): + """Wrap will patch the instance so that its queries are traced.""" + # ensure we've patched extensions (this is idempotent) in + # case we're only tracing some connections. + _patch_extensions(_psycopg2_extensions) + + c = traced_conn_cls(conn) + + # fetch tags from the dsn + dsn = sql.parse_pg_dsn(conn.dsn) + tags = { + net.TARGET_HOST: dsn.get("host"), + net.TARGET_PORT: dsn.get("port"), + db.NAME: dsn.get("dbname"), + db.USER: dsn.get("user"), + "db.application": dsn.get("application_name"), + } + + Pin(app="postgres", tags=tags).onto(c) + + return c + + +def _patch_extensions(_extensions): + # we must patch extensions all the time (it's pretty harmless) so split + # from global patching of connections. must be idempotent. + for _, module, func, wrapper in _extensions: + if not hasattr(module, func) or isinstance(getattr(module, func), wrapt.ObjectProxy): + continue + wrapt.wrap_function_wrapper(module, func, wrapper) + + +def _unpatch_extensions(_extensions): + # we must patch extensions all the time (it's pretty harmless) so split + # from global patching of connections. must be idempotent. + for original, module, func, _ in _extensions: + setattr(module, func, original) + + +# +# monkeypatch targets +# + + +def patched_connect(connect_func, _, args, kwargs): + conn = connect_func(*args, **kwargs) + return patch_conn(conn) + + +def _extensions_register_type(func, _, args, kwargs): + def _unroll_args(obj, scope=None): + return obj, scope + + obj, scope = _unroll_args(*args, **kwargs) + + # register_type performs a c-level check of the object + # type so we must be sure to pass in the actual db connection + if scope and isinstance(scope, wrapt.ObjectProxy): + scope = scope.__wrapped__ + + return func(obj, scope) if scope else func(obj) + + +def _extensions_quote_ident(func, _, args, kwargs): + def _unroll_args(obj, scope=None): + return obj, scope + + obj, scope = _unroll_args(*args, **kwargs) + + # register_type performs a c-level check of the object + # type so we must be sure to pass in the actual db connection + if scope and isinstance(scope, wrapt.ObjectProxy): + scope = scope.__wrapped__ + + return func(obj, scope) if scope else func(obj) + + +def _extensions_adapt(func, _, args, kwargs): + adapt = func(*args, **kwargs) + if hasattr(adapt, "prepare"): + return AdapterWrapper(adapt) + return adapt + + +class AdapterWrapper(wrapt.ObjectProxy): + def prepare(self, *args, **kwargs): + func = self.__wrapped__.prepare + if not args: + return func(*args, **kwargs) + conn = args[0] + + # prepare performs a c-level check of the object type so + # we must be sure to pass in the actual db connection + if isinstance(conn, wrapt.ObjectProxy): + conn = conn.__wrapped__ + + return func(conn, *args[1:], **kwargs) + + +# extension hooks +_psycopg2_extensions = [ + (psycopg2.extensions.register_type, psycopg2.extensions, "register_type", _extensions_register_type), + (psycopg2._psycopg.register_type, psycopg2._psycopg, "register_type", _extensions_register_type), + (psycopg2.extensions.adapt, psycopg2.extensions, "adapt", _extensions_adapt), +] + +# `_json` attribute is only available for psycopg >= 2.5 +if getattr(psycopg2, "_json", None): + _psycopg2_extensions += [ + (psycopg2._json.register_type, psycopg2._json, "register_type", _extensions_register_type), + ] + +# `quote_ident` attribute is only available for psycopg >= 2.7 +if getattr(psycopg2, "extensions", None) and getattr(psycopg2.extensions, "quote_ident", None): + _psycopg2_extensions += [ + (psycopg2.extensions.quote_ident, psycopg2.extensions, "quote_ident", _extensions_quote_ident), + ] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/__init__.py new file mode 100644 index 000000000..1fc0595e7 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/__init__.py @@ -0,0 +1,32 @@ +"""Instrument pylibmc to report Memcached queries. + +``patch_all`` will automatically patch your pylibmc client to make it work. +:: + + # Be sure to import pylibmc and not pylibmc.Client directly, + # otherwise you won't have access to the patched version + from ddtrace import Pin, patch + import pylibmc + + # If not patched yet, you can patch pylibmc specifically + patch(pylibmc=True) + + # One client instrumented with default configuration + client = pylibmc.Client(["localhost:11211"] + client.set("key1", "value1") + + # Use a pin to specify metadata related to this client + Pin.override(client, service="memcached-sessions") +""" + +from ...utils.importlib import require_modules + + +required_modules = ["pylibmc"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .client import TracedClient + from .patch import patch + + __all__ = ["TracedClient", "patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..8e121b5bd Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/__pycache__/addrs.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/__pycache__/addrs.cpython-38.pyc new file mode 100644 index 000000000..72e483509 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/__pycache__/addrs.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/__pycache__/client.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/__pycache__/client.cpython-38.pyc new file mode 100644 index 000000000..e6a6fbc4f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/__pycache__/client.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..bf4d53bc1 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/addrs.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/addrs.py new file mode 100644 index 000000000..0f11d2ac4 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/addrs.py @@ -0,0 +1,14 @@ +translate_server_specs = None + +try: + # NOTE: we rely on an undocumented method to parse addresses, + # so be a bit defensive and don't assume it exists. + from pylibmc.client import translate_server_specs +except ImportError: + pass + + +def parse_addresses(addrs): + if not translate_server_specs: + return [] + return translate_server_specs(addrs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/client.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/client.py new file mode 100644 index 000000000..03b05ccbe --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/client.py @@ -0,0 +1,159 @@ +from contextlib import contextmanager +import random + +import pylibmc + +# project +import ddtrace +from ddtrace import config +from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY +from ddtrace.constants import SPAN_MEASURED_KEY +from ddtrace.contrib.pylibmc.addrs import parse_addresses +from ddtrace.ext import SpanTypes +from ddtrace.ext import memcached +from ddtrace.ext import net +from ddtrace.internal.logger import get_logger +from ddtrace.vendor.wrapt import ObjectProxy + + +# Original Client class +_Client = pylibmc.Client + + +log = get_logger(__name__) + + +class TracedClient(ObjectProxy): + """TracedClient is a proxy for a pylibmc.Client that times it's network operations.""" + + def __init__(self, client=None, service=memcached.SERVICE, tracer=None, *args, **kwargs): + """Create a traced client that wraps the given memcached client.""" + # The client instance/service/tracer attributes are kept for compatibility + # with the old interface: TracedClient(client=pylibmc.Client(['localhost:11211'])) + # TODO(Benjamin): Remove these in favor of patching. + if not isinstance(client, _Client): + # We are in the patched situation, just pass down all arguments to the pylibmc.Client + # Note that, in that case, client isn't a real client (just the first argument) + client = _Client(client, *args, **kwargs) + else: + log.warning( + "TracedClient instantiation is deprecated and will be remove " + "in future versions (0.6.0). Use patching instead (see the docs)." + ) + + super(TracedClient, self).__init__(client) + + pin = ddtrace.Pin(service=service, tracer=tracer) + pin.onto(self) + + # attempt to collect the pool of urls this client talks to + try: + self._addresses = parse_addresses(client.addresses) + except Exception: + log.debug("error setting addresses", exc_info=True) + + def clone(self, *args, **kwargs): + # rewrap new connections. + cloned = self.__wrapped__.clone(*args, **kwargs) + traced_client = TracedClient(cloned) + pin = ddtrace.Pin.get_from(self) + if pin: + pin.clone().onto(traced_client) + return traced_client + + def get(self, *args, **kwargs): + return self._trace_cmd("get", *args, **kwargs) + + def set(self, *args, **kwargs): + return self._trace_cmd("set", *args, **kwargs) + + def delete(self, *args, **kwargs): + return self._trace_cmd("delete", *args, **kwargs) + + def gets(self, *args, **kwargs): + return self._trace_cmd("gets", *args, **kwargs) + + def touch(self, *args, **kwargs): + return self._trace_cmd("touch", *args, **kwargs) + + def cas(self, *args, **kwargs): + return self._trace_cmd("cas", *args, **kwargs) + + def incr(self, *args, **kwargs): + return self._trace_cmd("incr", *args, **kwargs) + + def decr(self, *args, **kwargs): + return self._trace_cmd("decr", *args, **kwargs) + + def append(self, *args, **kwargs): + return self._trace_cmd("append", *args, **kwargs) + + def prepend(self, *args, **kwargs): + return self._trace_cmd("prepend", *args, **kwargs) + + def get_multi(self, *args, **kwargs): + return self._trace_multi_cmd("get_multi", *args, **kwargs) + + def set_multi(self, *args, **kwargs): + return self._trace_multi_cmd("set_multi", *args, **kwargs) + + def delete_multi(self, *args, **kwargs): + return self._trace_multi_cmd("delete_multi", *args, **kwargs) + + def _trace_cmd(self, method_name, *args, **kwargs): + """trace the execution of the method with the given name and will + patch the first arg. + """ + method = getattr(self.__wrapped__, method_name) + with self._span(method_name) as span: + + if span and args: + span.set_tag(memcached.QUERY, "%s %s" % (method_name, args[0])) + + return method(*args, **kwargs) + + def _trace_multi_cmd(self, method_name, *args, **kwargs): + """trace the execution of the multi command with the given name.""" + method = getattr(self.__wrapped__, method_name) + with self._span(method_name) as span: + + pre = kwargs.get("key_prefix") + if span and pre: + span.set_tag(memcached.QUERY, "%s %s" % (method_name, pre)) + + return method(*args, **kwargs) + + @contextmanager + def _no_span(self): + yield None + + def _span(self, cmd_name): + """Return a span timing the given command.""" + pin = ddtrace.Pin.get_from(self) + if not pin or not pin.enabled(): + return self._no_span() + + span = pin.tracer.trace( + "memcached.cmd", + service=pin.service, + resource=cmd_name, + span_type=SpanTypes.CACHE, + ) + span.set_tag(SPAN_MEASURED_KEY) + + try: + self._tag_span(span) + except Exception: + log.debug("error tagging span", exc_info=True) + return span + + def _tag_span(self, span): + # FIXME[matt] the host selection is buried in c code. we can't tell what it's actually + # using, so fallback to randomly choosing one. can we do better? + if self._addresses: + _, host, port, _ = random.choice(self._addresses) + span.set_meta(net.TARGET_HOST, host) + span.set_meta(net.TARGET_PORT, port) + + # set analytics sample rate + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.pylibmc.get_analytics_sample_rate()) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/patch.py new file mode 100644 index 000000000..5b5b3d821 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylibmc/patch.py @@ -0,0 +1,15 @@ +import pylibmc + +from .client import TracedClient + + +# Original Client class +_Client = pylibmc.Client + + +def patch(): + setattr(pylibmc, "Client", TracedClient) + + +def unpatch(): + setattr(pylibmc, "Client", _Client) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__init__.py new file mode 100644 index 000000000..78404319f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__init__.py @@ -0,0 +1,71 @@ +""" +The Pylons__ integration traces requests and template rendering in a Pylons +application. + + +Enabling +~~~~~~~~ + +To enable the Pylons integration, wrap a Pylons application with the provided +``PylonsTraceMiddleware``:: + + from pylons.wsgiapp import PylonsApp + + from ddtrace import tracer + from ddtrace.contrib.pylons import PylonsTraceMiddleware + + app = PylonsApp(...) + + traced_app = PylonsTraceMiddleware(app, tracer, service="my-pylons-app") + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.pylons['distributed_tracing'] + + Whether to parse distributed tracing headers from requests received by your pylons app. + + Can also be enabled with the ``DD_PYLONS_DISTRIBUTED_TRACING`` environment variable. + + Default: ``True`` + + Example:: + + from ddtrace import config + + # Enable distributed tracing + config.pylons['distributed_tracing'] = True + + +.. py:data:: ddtrace.config.pylons["service"] + + The service name reported by default for Pylons requests. + + This option can also be set with the ``DD_SERVICE`` environment + variable. + + Default: ``"pylons"`` + + +:ref:`All HTTP tags ` are supported for this integration. + +.. __: https://pylonsproject.org/about-pylons-framework.html +""" + +from ...utils.importlib import require_modules + + +required_modules = ["pylons.wsgiapp"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .middleware import PylonsTraceMiddleware + from .patch import patch + from .patch import unpatch + + __all__ = [ + "patch", + "unpatch", + "PylonsTraceMiddleware", + ] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..b3f5b1933 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/compat.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/compat.cpython-38.pyc new file mode 100644 index 000000000..be8c6cbf8 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/compat.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/constants.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/constants.cpython-38.pyc new file mode 100644 index 000000000..fda344877 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/constants.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/middleware.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/middleware.cpython-38.pyc new file mode 100644 index 000000000..81672a983 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/middleware.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..2ab4de1ce Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/renderer.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/renderer.cpython-38.pyc new file mode 100644 index 000000000..e9d7f303e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/__pycache__/renderer.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/compat.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/compat.py new file mode 100644 index 000000000..f49480d05 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/compat.py @@ -0,0 +1,8 @@ +try: + from pylons.templating import render_mako # noqa + + # Pylons > 0.9.7 + legacy_pylons = False +except ImportError: + # Pylons <= 0.9.7 + legacy_pylons = True diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/constants.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/constants.py new file mode 100644 index 000000000..082befab6 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/constants.py @@ -0,0 +1 @@ +CONFIG_MIDDLEWARE = "__datadog_middleware" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/middleware.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/middleware.py new file mode 100644 index 000000000..1b2224b06 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/middleware.py @@ -0,0 +1,124 @@ +import sys + +from pylons import config +from webob import Request + +from ddtrace import config as ddconfig + +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import http +from ...internal.compat import reraise +from ...internal.logger import get_logger +from ...utils.formats import asbool +from .constants import CONFIG_MIDDLEWARE +from .renderer import trace_rendering + + +log = get_logger(__name__) + + +class PylonsTraceMiddleware(object): + def __init__(self, app, tracer, service="pylons", distributed_tracing=None): + self.app = app + self._service = service + self._tracer = tracer + + if distributed_tracing is not None: + self._distributed_tracing = distributed_tracing + + # register middleware reference + config[CONFIG_MIDDLEWARE] = self + + # add template tracing + trace_rendering() + + @property + def _distributed_tracing(self): + return ddconfig.pylons.distributed_tracing + + @_distributed_tracing.setter + def _distributed_tracing(self, distributed_tracing): + ddconfig.pylons["distributed_tracing"] = asbool(distributed_tracing) + + def __call__(self, environ, start_response): + request = Request(environ) + trace_utils.activate_distributed_headers( + self._tracer, int_config=ddconfig.pylons, request_headers=request.headers + ) + + with self._tracer.trace("pylons.request", service=self._service, span_type=SpanTypes.WEB) as span: + span.set_tag(SPAN_MEASURED_KEY) + # Set the service in tracer.trace() as priority sampling requires it to be + # set as early as possible when different services share one single agent. + + # set analytics sample rate with global config enabled + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, ddconfig.pylons.get_analytics_sample_rate(use_global_config=True)) + + trace_utils.set_http_meta(span, ddconfig.pylons, request_headers=request.headers) + + if not span.sampled: + return self.app(environ, start_response) + + # tentative on status code, otherwise will be caught by except below + def _start_response(status, *args, **kwargs): + """a patched response callback which will pluck some metadata.""" + if len(args): + response_headers = args[0] + else: + response_headers = kwargs.get("response_headers", {}) + http_code = int(status.split()[0]) + trace_utils.set_http_meta( + span, ddconfig.pylons, status_code=http_code, response_headers=response_headers + ) + return start_response(status, *args, **kwargs) + + try: + return self.app(environ, _start_response) + except Exception as e: + # store current exceptions info so we can re-raise it later + (typ, val, tb) = sys.exc_info() + + # e.code can either be a string or an int + code = getattr(e, "code", 500) + try: + int(code) + except (TypeError, ValueError): + code = 500 + trace_utils.set_http_meta(span, ddconfig.pylons, status_code=code) + + # re-raise the original exception with its original traceback + reraise(typ, val, tb=tb) + except SystemExit: + span.set_tag(http.STATUS_CODE, 500) + span.error = 1 + raise + finally: + controller = environ.get("pylons.routes_dict", {}).get("controller") + action = environ.get("pylons.routes_dict", {}).get("action") + + # There are cases where users re-route requests and manually + # set resources. If this is so, don't do anything, otherwise + # set the resource to the controller / action that handled it. + if span.resource == span.name: + span.resource = "%s.%s" % (controller, action) + + url = "%s://%s:%s%s" % ( + environ.get("wsgi.url_scheme"), + environ.get("SERVER_NAME"), + environ.get("SERVER_PORT"), + environ.get("PATH_INFO"), + ) + trace_utils.set_http_meta( + span, + ddconfig.pylons, + method=environ.get("REQUEST_METHOD"), + url=url, + query=environ.get("QUERY_STRING"), + ) + if controller: + span._set_str_tag("pylons.route.controller", controller) + if action: + span._set_str_tag("pylons.route.action", action) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/patch.py new file mode 100644 index 000000000..f79fbfa23 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/patch.py @@ -0,0 +1,52 @@ +import pylons.wsgiapp + +from ddtrace import Pin +from ddtrace import config +from ddtrace import tracer +from ddtrace.vendor import wrapt + +from ...utils.formats import asbool +from ...utils.formats import get_env +from ...utils.wrappers import unwrap as _u +from .middleware import PylonsTraceMiddleware + + +config._add( + "pylons", + dict( + distributed_tracing=asbool(get_env("pylons", "distributed_tracing", default=True)), + ), +) + + +def patch(): + """Instrument Pylons applications""" + if getattr(pylons.wsgiapp, "_datadog_patch", False): + return + + setattr(pylons.wsgiapp, "_datadog_patch", True) + wrapt.wrap_function_wrapper("pylons.wsgiapp", "PylonsApp.__init__", traced_init) + + +def unpatch(): + """Disable Pylons tracing""" + if not getattr(pylons.wsgiapp, "__datadog_patch", False): + return + setattr(pylons.wsgiapp, "__datadog_patch", False) + + _u(pylons.wsgiapp.PylonsApp, "__init__") + + +def traced_init(wrapped, instance, args, kwargs): + wrapped(*args, **kwargs) + + # set tracing options and create the TraceMiddleware + service = config._get_service(default="pylons") + Pin(service=service, tracer=tracer).onto(instance) + traced_app = PylonsTraceMiddleware( + instance, tracer, service=service, distributed_tracing=config.pylons.distributed_tracing + ) + + # re-order the middleware stack so that the first middleware is ours + traced_app.app = instance.app + instance.app = traced_app diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/renderer.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/renderer.py new file mode 100644 index 000000000..fad0360e3 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pylons/renderer.py @@ -0,0 +1,37 @@ +import pylons +from pylons import config + +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from ...utils import get_argument_value +from .compat import legacy_pylons +from .constants import CONFIG_MIDDLEWARE + + +def trace_rendering(): + """Patch all Pylons renderers. It supports multiple versions + of Pylons and multiple renderers. + """ + # patch only once + if getattr(pylons.templating, "__datadog_patch", False): + return + setattr(pylons.templating, "__datadog_patch", True) + + if legacy_pylons: + # Pylons <= 0.9.7 + _w("pylons.templating", "render", _traced_renderer) + else: + # Pylons > 0.9.7 + _w("pylons.templating", "render_mako", _traced_renderer) + _w("pylons.templating", "render_mako_def", _traced_renderer) + _w("pylons.templating", "render_genshi", _traced_renderer) + _w("pylons.templating", "render_jinja2", _traced_renderer) + + +def _traced_renderer(wrapped, instance, args, kwargs): + """Traced renderer""" + tracer = config[CONFIG_MIDDLEWARE]._tracer + with tracer.trace("pylons.render") as span: + template_name = get_argument_value(args, kwargs, 0, "template_name") + span.set_tag("template.name", template_name) + return wrapped(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/__init__.py new file mode 100644 index 000000000..67676f7de --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/__init__.py @@ -0,0 +1,40 @@ +"""Instrument pymemcache to report memcached queries. + +``patch_all`` will automatically patch the pymemcache ``Client``:: + + from ddtrace import Pin, patch + + # If not patched yet, patch pymemcache specifically + patch(pymemcache=True) + + # Import reference to Client AFTER patching + import pymemcache + from pymemcache.client.base import Client + + # Use a pin to specify metadata related all clients + Pin.override(pymemcache, service='my-memcached-service') + + # This will report a span with the default settings + client = Client(('localhost', 11211)) + client.set("my-key", "my-val") + + # Use a pin to specify metadata related to this particular client + Pin.override(client, service='my-memcached-service') + +Pymemcache ``HashClient`` will also be indirectly patched as it uses ``Client`` +under the hood. +""" +from ...utils.importlib import require_modules + + +required_modules = ["pymemcache"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = [ + patch, + unpatch, + ] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..8687c799c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/__pycache__/client.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/__pycache__/client.cpython-38.pyc new file mode 100644 index 000000000..33025fadd Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/__pycache__/client.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..010d1d6a8 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/client.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/client.py new file mode 100644 index 000000000..5e6ab24f3 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/client.py @@ -0,0 +1,220 @@ +import sys + +import pymemcache +from pymemcache.client.base import Client +from pymemcache.exceptions import MemcacheClientError +from pymemcache.exceptions import MemcacheIllegalInputError +from pymemcache.exceptions import MemcacheServerError +from pymemcache.exceptions import MemcacheUnknownCommandError +from pymemcache.exceptions import MemcacheUnknownError + +# 3p +from ddtrace import config +from ddtrace.vendor import wrapt + +# project +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import memcached as memcachedx +from ...ext import net +from ...internal.compat import reraise +from ...internal.logger import get_logger +from ...pin import Pin + + +log = get_logger(__name__) + + +# keep a reference to the original unpatched clients +_Client = Client + + +class WrappedClient(wrapt.ObjectProxy): + """Wrapper providing patched methods of a pymemcache Client. + + Relevant connection information is obtained during initialization and + attached to each span. + + Keys are tagged in spans for methods that act upon a key. + """ + + def __init__(self, *args, **kwargs): + c = _Client(*args, **kwargs) + super(WrappedClient, self).__init__(c) + + # tags to apply to each span generated by this client + tags = _get_address_tags(*args, **kwargs) + + parent_pin = Pin.get_from(pymemcache) + + if parent_pin: + pin = parent_pin.clone(tags=tags) + else: + pin = Pin(tags=tags) + + # attach the pin onto this instance + pin.onto(self) + + def set(self, *args, **kwargs): + return self._traced_cmd("set", *args, **kwargs) + + def set_many(self, *args, **kwargs): + return self._traced_cmd("set_many", *args, **kwargs) + + def add(self, *args, **kwargs): + return self._traced_cmd("add", *args, **kwargs) + + def replace(self, *args, **kwargs): + return self._traced_cmd("replace", *args, **kwargs) + + def append(self, *args, **kwargs): + return self._traced_cmd("append", *args, **kwargs) + + def prepend(self, *args, **kwargs): + return self._traced_cmd("prepend", *args, **kwargs) + + def cas(self, *args, **kwargs): + return self._traced_cmd("cas", *args, **kwargs) + + def get(self, *args, **kwargs): + return self._traced_cmd("get", *args, **kwargs) + + def get_many(self, *args, **kwargs): + return self._traced_cmd("get_many", *args, **kwargs) + + def gets(self, *args, **kwargs): + return self._traced_cmd("gets", *args, **kwargs) + + def gets_many(self, *args, **kwargs): + return self._traced_cmd("gets_many", *args, **kwargs) + + def delete(self, *args, **kwargs): + return self._traced_cmd("delete", *args, **kwargs) + + def delete_many(self, *args, **kwargs): + return self._traced_cmd("delete_many", *args, **kwargs) + + def incr(self, *args, **kwargs): + return self._traced_cmd("incr", *args, **kwargs) + + def decr(self, *args, **kwargs): + return self._traced_cmd("decr", *args, **kwargs) + + def touch(self, *args, **kwargs): + return self._traced_cmd("touch", *args, **kwargs) + + def stats(self, *args, **kwargs): + return self._traced_cmd("stats", *args, **kwargs) + + def version(self, *args, **kwargs): + return self._traced_cmd("version", *args, **kwargs) + + def flush_all(self, *args, **kwargs): + return self._traced_cmd("flush_all", *args, **kwargs) + + def quit(self, *args, **kwargs): + return self._traced_cmd("quit", *args, **kwargs) + + def set_multi(self, *args, **kwargs): + """set_multi is an alias for set_many""" + return self._traced_cmd("set_many", *args, **kwargs) + + def get_multi(self, *args, **kwargs): + """set_multi is an alias for set_many""" + return self._traced_cmd("get_many", *args, **kwargs) + + def _traced_cmd(self, method_name, *args, **kwargs): + """Run and trace the given command. + + Any pymemcache exception is caught and span error information is + set. The exception is then reraised for the application to handle + appropriately. + + Relevant tags are set in the span. + """ + method = getattr(self.__wrapped__, method_name) + p = Pin.get_from(self) + + # if the pin does not exist or is not enabled, shortcut + if not p or not p.enabled(): + return method(*args, **kwargs) + + with p.tracer.trace( + memcachedx.CMD, + service=p.service, + resource=method_name, + span_type=SpanTypes.CACHE, + ) as span: + span.set_tag(SPAN_MEASURED_KEY) + # set analytics sample rate + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.pymemcache.get_analytics_sample_rate()) + + # try to set relevant tags, catch any exceptions so we don't mess + # with the application + try: + span.set_tags(p.tags) + vals = _get_query_string(args) + query = "{}{}{}".format(method_name, " " if vals else "", vals) + span.set_tag(memcachedx.QUERY, query) + except Exception: + log.debug("Error setting relevant pymemcache tags") + + try: + return method(*args, **kwargs) + except ( + MemcacheClientError, + MemcacheServerError, + MemcacheUnknownCommandError, + MemcacheUnknownError, + MemcacheIllegalInputError, + ): + (typ, val, tb) = sys.exc_info() + span.set_exc_info(typ, val, tb) + reraise(typ, val, tb) + + +def _get_address_tags(*args, **kwargs): + """Attempt to get host and port from args passed to Client initializer.""" + tags = {} + try: + if len(args): + host, port = args[0] + tags[net.TARGET_HOST] = host + tags[net.TARGET_PORT] = port + except Exception: + log.debug("Error collecting client address tags") + + return tags + + +def _get_query_string(args): + """Return the query values given the arguments to a pymemcache command. + + If there are multiple query values, they are joined together + space-separated. + """ + keys = "" + + # shortcut if no args + if not args: + return keys + + # pull out the first arg which will contain any key + arg = args[0] + + # if we get a dict, convert to list of keys + if type(arg) is dict: + arg = list(arg) + + if type(arg) is str: + keys = arg + elif type(arg) is bytes: + keys = arg.decode() + elif type(arg) is list and len(arg): + if type(arg[0]) is str: + keys = " ".join(arg) + elif type(arg[0]) is bytes: + keys = b" ".join(arg).decode() + + return keys diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/patch.py new file mode 100644 index 000000000..3ecc839c1 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymemcache/patch.py @@ -0,0 +1,46 @@ +import pymemcache +import pymemcache.client.hash + +from ddtrace.ext import memcached as memcachedx +from ddtrace.pin import Pin +from ddtrace.pin import _DD_PIN_NAME +from ddtrace.pin import _DD_PIN_PROXY_NAME + +from .client import WrappedClient + + +_Client = pymemcache.client.base.Client +_hash_Client = pymemcache.client.hash.Client + +_HashClient_client_class = None +if hasattr(pymemcache.client.hash.HashClient, "client_class"): + _HashClient_client_class = pymemcache.client.hash.HashClient.client_class + + +def patch(): + if getattr(pymemcache.client, "_datadog_patch", False): + return + + setattr(pymemcache.client, "_datadog_patch", True) + setattr(pymemcache.client.base, "Client", WrappedClient) + setattr(pymemcache.client.hash, "Client", WrappedClient) + if _HashClient_client_class: + pymemcache.client.hash.HashClient.client_class = WrappedClient + + # Create a global pin with default configuration for our pymemcache clients + Pin(app=memcachedx.SERVICE, service=memcachedx.SERVICE).onto(pymemcache) + + +def unpatch(): + """Remove pymemcache tracing""" + if not getattr(pymemcache.client, "_datadog_patch", False): + return + setattr(pymemcache.client, "_datadog_patch", False) + setattr(pymemcache.client.base, "Client", _Client) + setattr(pymemcache.client.hash, "Client", _hash_Client) + if _HashClient_client_class: + pymemcache.client.hash.HashClient.client_class = _HashClient_client_class + + # Remove any pins that may exist on the pymemcache reference + setattr(pymemcache, _DD_PIN_NAME, None) + setattr(pymemcache, _DD_PIN_PROXY_NAME, None) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/__init__.py new file mode 100644 index 000000000..0ce08eb66 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/__init__.py @@ -0,0 +1,37 @@ +"""Instrument pymongo to report MongoDB queries. + +The pymongo integration works by wrapping pymongo's MongoClient to trace +network calls. Pymongo 3.0 and greater are the currently supported versions. +``patch_all`` will automatically patch your MongoClient instance to make it work. + +:: + + # Be sure to import pymongo and not pymongo.MongoClient directly, + # otherwise you won't have access to the patched version + from ddtrace import Pin, patch + import pymongo + + # If not patched yet, you can patch pymongo specifically + patch(pymongo=True) + + # At that point, pymongo is instrumented with the default settings + client = pymongo.MongoClient() + # Example of instrumented query + db = client["test-db"] + db.teams.find({"name": "Toronto Maple Leafs"}) + + # Use a pin to specify metadata related to this client + client = pymongo.MongoClient() + pin = Pin.override(client, service="mongo-master") +""" +from ...utils.importlib import require_modules + + +required_modules = ["pymongo"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import trace_mongo_client + + __all__ = ["trace_mongo_client", "patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..eb1f9cca3 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/__pycache__/client.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/__pycache__/client.cpython-38.pyc new file mode 100644 index 000000000..565a15090 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/__pycache__/client.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/__pycache__/parse.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/__pycache__/parse.cpython-38.pyc new file mode 100644 index 000000000..3ecc77544 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/__pycache__/parse.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..0c2ce919c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/client.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/client.py new file mode 100644 index 000000000..8b54e5199 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/client.py @@ -0,0 +1,270 @@ +# stdlib +import contextlib +import json + +# 3p +import pymongo + +# project +import ddtrace +from ddtrace import config +from ddtrace.vendor.wrapt import ObjectProxy + +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import mongo as mongox +from ...ext import net as netx +from ...internal.compat import iteritems +from ...internal.logger import get_logger +from .parse import parse_msg +from .parse import parse_query +from .parse import parse_spec + + +# Original Client class +_MongoClient = pymongo.MongoClient + +log = get_logger(__name__) + + +VERSION = pymongo.version_tuple + + +class TracedMongoClient(ObjectProxy): + def __init__(self, client=None, *args, **kwargs): + # To support the former trace_mongo_client interface, we have to keep this old interface + # TODO(Benjamin): drop it in a later version + if not isinstance(client, _MongoClient): + # Patched interface, instantiate the client + + # client is just the first arg which could be the host if it is + # None, then it could be that the caller: + + # if client is None then __init__ was: + # 1) invoked with host=None + # 2) not given a first argument (client defaults to None) + # we cannot tell which case it is, but it should not matter since + # the default value for host is None, in either case we can simply + # not provide it as an argument + if client is None: + client = _MongoClient(*args, **kwargs) + # else client is a value for host so just pass it along + else: + client = _MongoClient(client, *args, **kwargs) + + super(TracedMongoClient, self).__init__(client) + # NOTE[matt] the TracedMongoClient attempts to trace all of the network + # calls in the trace library. This is good because it measures the + # actual network time. It's bad because it uses a private API which + # could change. We'll see how this goes. + if not isinstance(client._topology, TracedTopology): + client._topology = TracedTopology(client._topology) + + # Default Pin + ddtrace.Pin(service=mongox.SERVICE, app=mongox.SERVICE).onto(self) + + def __setddpin__(self, pin): + pin.onto(self._topology) + + def __getddpin__(self): + return ddtrace.Pin.get_from(self._topology) + + +class TracedTopology(ObjectProxy): + def __init__(self, topology): + super(TracedTopology, self).__init__(topology) + + def select_server(self, *args, **kwargs): + s = self.__wrapped__.select_server(*args, **kwargs) + if not isinstance(s, TracedServer): + s = TracedServer(s) + # Reattach the pin every time in case it changed since the initial patching + ddtrace.Pin.get_from(self).onto(s) + return s + + +class TracedServer(ObjectProxy): + def __init__(self, server): + super(TracedServer, self).__init__(server) + + def _datadog_trace_operation(self, operation): + cmd = None + # Only try to parse something we think is a query. + if self._is_query(operation): + try: + cmd = parse_query(operation) + except Exception: + log.exception("error parsing query") + + pin = ddtrace.Pin.get_from(self) + # if we couldn't parse or shouldn't trace the message, just go. + if not cmd or not pin or not pin.enabled(): + return None + + span = pin.tracer.trace("pymongo.cmd", span_type=SpanTypes.MONGODB, service=pin.service) + span.set_tag(SPAN_MEASURED_KEY) + span.set_tag(mongox.DB, cmd.db) + span.set_tag(mongox.COLLECTION, cmd.coll) + span.set_tags(cmd.tags) + + # set `mongodb.query` tag and resource for span + _set_query_metadata(span, cmd) + + # set analytics sample rate + sample_rate = config.pymongo.get_analytics_sample_rate() + if sample_rate is not None: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate) + return span + + # Pymongo >= 3.12 + if VERSION >= (3, 12, 0): + + def run_operation(self, sock_info, operation, *args, **kwargs): + with self._datadog_trace_operation(operation) as span: + result = self.__wrapped__.run_operation(sock_info, operation, *args, **kwargs) + if result and result.address: + set_address_tags(span, result.address) + return result + + # Pymongo >= 3.9, <3.12 + elif (3, 9, 0) <= VERSION < (3, 12, 0): + + def run_operation_with_response(self, sock_info, operation, *args, **kwargs): + with self._datadog_trace_operation(operation) as span: + result = self.__wrapped__.run_operation_with_response(sock_info, operation, *args, **kwargs) + if result and result.address: + set_address_tags(span, result.address) + return result + + # Pymongo < 3.9 + else: + + def send_message_with_response(self, operation, *args, **kwargs): + with self._datadog_trace_operation(operation) as span: + result = self.__wrapped__.send_message_with_response(operation, *args, **kwargs) + if result and result.address: + set_address_tags(span, result.address) + return result + + @contextlib.contextmanager + def get_socket(self, *args, **kwargs): + with self.__wrapped__.get_socket(*args, **kwargs) as s: + if not isinstance(s, TracedSocket): + s = TracedSocket(s) + ddtrace.Pin.get_from(self).onto(s) + yield s + + @staticmethod + def _is_query(op): + # NOTE: _Query should always have a spec field + return hasattr(op, "spec") + + +class TracedSocket(ObjectProxy): + def __init__(self, socket): + super(TracedSocket, self).__init__(socket) + + def command(self, dbname, spec, *args, **kwargs): + cmd = None + try: + cmd = parse_spec(spec, dbname) + except Exception: + log.exception("error parsing spec. skipping trace") + + pin = ddtrace.Pin.get_from(self) + # skip tracing if we don't have a piece of data we need + if not dbname or not cmd or not pin or not pin.enabled(): + return self.__wrapped__.command(dbname, spec, *args, **kwargs) + + cmd.db = dbname + with self.__trace(cmd): + return self.__wrapped__.command(dbname, spec, *args, **kwargs) + + def write_command(self, request_id, msg): + cmd = None + try: + cmd = parse_msg(msg) + except Exception: + log.exception("error parsing msg") + + pin = ddtrace.Pin.get_from(self) + # if we couldn't parse it, don't try to trace it. + if not cmd or not pin or not pin.enabled(): + return self.__wrapped__.write_command(request_id, msg) + + with self.__trace(cmd) as s: + result = self.__wrapped__.write_command(request_id, msg) + if result: + s.set_metric(mongox.ROWS, result.get("n", -1)) + return result + + def __trace(self, cmd): + pin = ddtrace.Pin.get_from(self) + s = pin.tracer.trace("pymongo.cmd", span_type=SpanTypes.MONGODB, service=pin.service) + + s.set_tag(SPAN_MEASURED_KEY) + if cmd.db: + s.set_tag(mongox.DB, cmd.db) + if cmd: + s.set_tag(mongox.COLLECTION, cmd.coll) + s.set_tags(cmd.tags) + s.set_metrics(cmd.metrics) + + # set `mongodb.query` tag and resource for span + _set_query_metadata(s, cmd) + + # set analytics sample rate + s.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.pymongo.get_analytics_sample_rate()) + + if self.address: + set_address_tags(s, self.address) + return s + + +def normalize_filter(f=None): + if f is None: + return {} + elif isinstance(f, list): + # normalize lists of filters + # e.g. {$or: [ { age: { $lt: 30 } }, { type: 1 } ]} + return [normalize_filter(s) for s in f] + elif isinstance(f, dict): + # normalize dicts of filters + # {$or: [ { age: { $lt: 30 } }, { type: 1 } ]}) + out = {} + for k, v in iteritems(f): + if k == "$in" or k == "$nin": + # special case $in queries so we don't loop over lists. + out[k] = "?" + elif isinstance(v, list) or isinstance(v, dict): + # RECURSION ALERT: needs to move to the agent + out[k] = normalize_filter(v) + else: + # NOTE: this shouldn't happen, but let's have a safeguard. + out[k] = "?" + return out + else: + # FIXME[matt] unexpected type. not sure this should ever happen, but at + # least it won't crash. + return {} + + +def set_address_tags(span, address): + # the address is only set after the cursor is done. + if address: + span.set_tag(netx.TARGET_HOST, address[0]) + span.set_tag(netx.TARGET_PORT, address[1]) + + +def _set_query_metadata(span, cmd): + """Sets span `mongodb.query` tag and resource given command query""" + if cmd.query: + nq = normalize_filter(cmd.query) + span.set_tag("mongodb.query", nq) + # needed to dump json so we don't get unicode + # dict keys like {u'foo':'bar'} + q = json.dumps(nq) + span.resource = "{} {} {}".format(cmd.name, cmd.coll, q) + else: + span.resource = "{} {}".format(cmd.name, cmd.coll) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/parse.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/parse.py new file mode 100644 index 000000000..1a4330d2e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/parse.py @@ -0,0 +1,204 @@ +import ctypes +import struct + +# 3p +import bson +from bson.codec_options import CodecOptions +from bson.son import SON + +# project +from ...ext import net as netx +from ...internal.compat import to_unicode +from ...internal.logger import get_logger + + +log = get_logger(__name__) + + +# MongoDB wire protocol commands +# http://docs.mongodb.com/manual/reference/mongodb-wire-protocol +OP_CODES = { + 1: "reply", + 1000: "msg", # DEV: 1000 was deprecated at some point, use 2013 instead + 2001: "update", + 2002: "insert", + 2003: "reserved", + 2004: "query", + 2005: "get_more", + 2006: "delete", + 2007: "kill_cursors", + 2010: "command", + 2011: "command_reply", + 2013: "msg", +} + +# The maximum message length we'll try to parse +MAX_MSG_PARSE_LEN = 1024 * 1024 + +header_struct = struct.Struct("= 3.1 stores the db and coll separately + coll = getattr(query, "coll", None) + db = getattr(query, "db", None) + + # pymongo < 3.1 _Query does not have a name field, so default to 'query' + cmd = Command(getattr(query, "name", "query"), db, coll) + cmd.query = query.spec + return cmd + + +def parse_spec(spec, db=None): + """Return a Command that has parsed the relevant detail for the given + pymongo SON spec. + """ + + # the first element is the command and collection + items = list(spec.items()) + if not items: + return None + name, coll = items[0] + cmd = Command(name, db or spec.get("$db"), coll) + + if "ordered" in spec: # in insert and update + cmd.tags["mongodb.ordered"] = spec["ordered"] + + if cmd.name == "insert": + if "documents" in spec: + cmd.metrics["mongodb.documents"] = len(spec["documents"]) + + elif cmd.name == "update": + updates = spec.get("updates") + if updates: + # FIXME[matt] is there ever more than one here? + cmd.query = updates[0].get("q") + + elif cmd.name == "delete": + dels = spec.get("deletes") + if dels: + # FIXME[matt] is there ever more than one here? + cmd.query = dels[0].get("q") + + return cmd + + +def _cstring(raw): + """Return the first null terminated cstring from the buffer.""" + return ctypes.create_string_buffer(raw).value + + +def _split_namespace(ns): + """Return a tuple of (db, collection) from the 'db.coll' string.""" + if ns: + # NOTE[matt] ns is unicode or bytes depending on the client version + # so force cast to unicode + split = to_unicode(ns).split(".", 1) + if len(split) == 1: + raise Exception("namespace doesn't contain period: %s" % ns) + return split + return (None, None) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/patch.py new file mode 100644 index 000000000..5aeaec674 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymongo/patch.py @@ -0,0 +1,84 @@ +import contextlib + +import pymongo + +from ddtrace import Pin +from ddtrace import config +from ddtrace.contrib import trace_utils +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import mongo as mongox +from ...utils.deprecation import deprecated +from ...utils.wrappers import unwrap as _u +from .client import TracedMongoClient +from .client import set_address_tags + + +config._add( + "pymongo", + dict( + _default_service=mongox.SERVICE, + ), +) + + +# Original Client class +_MongoClient = pymongo.MongoClient + + +def patch(): + patch_pymongo_module() + # We should progressively get rid of TracedMongoClient. We now try to + # wrap methods individually. cf #1501 + setattr(pymongo, "MongoClient", TracedMongoClient) + + +def unpatch(): + unpatch_pymongo_module() + setattr(pymongo, "MongoClient", _MongoClient) + + +@deprecated(message="Use patching instead (see the docs).", version="1.0.0") +def trace_mongo_client(client, tracer, service=mongox.SERVICE): + traced_client = TracedMongoClient(client) + Pin(service=service, tracer=tracer).onto(traced_client) + return traced_client + + +def patch_pymongo_module(): + if getattr(pymongo, "_datadog_patch", False): + return + setattr(pymongo, "_datadog_patch", True) + Pin(app=mongox.SERVICE).onto(pymongo.server.Server) + + # Whenever a pymongo command is invoked, the lib either: + # - Creates a new socket & performs a TCP handshake + # - Grabs a socket already initialized before + _w("pymongo.server", "Server.get_socket", traced_get_socket) + + +def unpatch_pymongo_module(): + if not getattr(pymongo, "_datadog_patch", False): + return + setattr(pymongo, "_datadog_patch", False) + + _u(pymongo.server.Server, "get_socket") + + +@contextlib.contextmanager +def traced_get_socket(wrapped, instance, args, kwargs): + pin = Pin._find(wrapped, instance) + if not pin or not pin.enabled(): + with wrapped(*args, **kwargs) as sock_info: + yield sock_info + return + + with pin.tracer.trace( + "pymongo.get_socket", service=trace_utils.int_service(pin, config.pymongo), span_type=SpanTypes.MONGODB + ) as span: + with wrapped(*args, **kwargs) as sock_info: + set_address_tags(span, sock_info.address) + span.set_tag(SPAN_MEASURED_KEY) + yield sock_info diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/__init__.py new file mode 100644 index 000000000..184952e31 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/__init__.py @@ -0,0 +1,68 @@ +""" +The pymysql integration instruments the pymysql library to trace MySQL queries. + + +Enabling +~~~~~~~~ + +The integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(pymysql=True) + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.pymysql["service"] + + The service name reported by default for pymysql spans. + + This option can also be set with the ``DD_PYMYSQL_SERVICE`` environment + variable. + + Default: ``"mysql"`` + +.. py:data:: ddtrace.config.pymysql["trace_fetch_methods"] + + Whether or not to trace fetch methods. + + Can also configured via the ``DD_PYMYSQL_TRACE_FETCH_METHODS`` environment variable. + + Default: ``False`` + + +Instance Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +To configure the integration on an per-connection basis use the +``Pin`` API:: + + from ddtrace import Pin + from pymysql import connect + + # This will report a span with the default settings + conn = connect(user="alice", password="b0b", host="localhost", port=3306, database="test") + + # Use a pin to override the service name for this connection. + Pin.override(conn, service="pymysql-users") + + + cursor = conn.cursor() + cursor.execute("SELECT 6*7 AS the_answer;") +""" + +from ...utils.importlib import require_modules + + +required_modules = ["pymysql"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .tracers import get_traced_pymysql_connection + + __all__ = ["get_traced_pymysql_connection", "patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..3ea73cbcb Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..1103c87ef Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/__pycache__/tracers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/__pycache__/tracers.cpython-38.pyc new file mode 100644 index 000000000..bf8f45c1b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/__pycache__/tracers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/patch.py new file mode 100644 index 000000000..9988c6e76 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/patch.py @@ -0,0 +1,54 @@ +# 3p +import pymysql + +# project +from ddtrace import Pin +from ddtrace import config +from ddtrace.contrib.dbapi import TracedConnection +from ddtrace.vendor import wrapt + +from ...ext import db +from ...ext import net +from ...utils.formats import asbool +from ...utils.formats import get_env + + +config._add( + "pymysql", + dict( + # TODO[v1.0] this should be "mysql" + _default_service="pymysql", + trace_fetch_methods=asbool(get_env("pymysql", "trace_fetch_methods", default=False)), + ), +) + +CONN_ATTR_BY_TAG = { + net.TARGET_HOST: "host", + net.TARGET_PORT: "port", + db.USER: "user", + db.NAME: "db", +} + + +def patch(): + wrapt.wrap_function_wrapper("pymysql", "connect", _connect) + + +def unpatch(): + if isinstance(pymysql.connect, wrapt.ObjectProxy): + pymysql.connect = pymysql.connect.__wrapped__ + + +def _connect(func, instance, args, kwargs): + conn = func(*args, **kwargs) + return patch_conn(conn) + + +def patch_conn(conn): + tags = {t: getattr(conn, a, "") for t, a in CONN_ATTR_BY_TAG.items()} + pin = Pin(app="pymysql", tags=tags) + + # grab the metadata from the conn + wrapped = TracedConnection(conn, pin=pin, cfg=config.pymysql) + pin.onto(wrapped) + return wrapped diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/tracers.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/tracers.py new file mode 100644 index 000000000..a80e8092a --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pymysql/tracers.py @@ -0,0 +1,8 @@ +import pymysql.connections + +from ...utils.deprecation import deprecated + + +@deprecated(message="Use patching instead (see the docs).", version="1.0.0") +def get_traced_pymysql_connection(*args, **kwargs): + return pymysql.connections.Connection diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pynamodb/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pynamodb/__init__.py new file mode 100644 index 000000000..4fa099557 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pynamodb/__init__.py @@ -0,0 +1,41 @@ +""" +The PynamoDB integration traces all db calls made with the pynamodb +library through the connection API. + +Enabling +~~~~~~~~ + +The PynamoDB integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + import pynamodb + from ddtrace import patch, config + patch(pynamodb=True) + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.pynamodb["service"] + + The service name reported by default for the PynamoDB instance. + + This option can also be set with the ``DD_PYNAMODB_SERVICE`` environment + variable. + + Default: ``"pynamodb"`` + +""" + + +from ...utils.importlib import require_modules + + +required_modules = ["pynamodb.connection.base"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + + __all__ = ["patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pynamodb/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pynamodb/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..a95fb6093 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pynamodb/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pynamodb/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pynamodb/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..1128365fd Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pynamodb/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pynamodb/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pynamodb/patch.py new file mode 100644 index 000000000..bce9e4000 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pynamodb/patch.py @@ -0,0 +1,89 @@ +""" +Trace queries to botocore api done via a pynamodb client +""" + +import pynamodb.connection.base + +from ddtrace import config +from ddtrace.vendor import wrapt + +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...pin import Pin +from ...utils import ArgumentError +from ...utils import get_argument_value +from ...utils.formats import deep_getattr +from ...utils.wrappers import unwrap + + +# Pynamodb connection class +_PynamoDB_client = pynamodb.connection.base.Connection + +config._add( + "pynamodb", + { + "_default_service": "pynamodb", + }, +) + + +def patch(): + if getattr(pynamodb.connection.base, "_datadog_patch", False): + return + setattr(pynamodb.connection.base, "_datadog_patch", True) + + wrapt.wrap_function_wrapper("pynamodb.connection.base", "Connection._make_api_call", patched_api_call) + Pin(service=None).onto(pynamodb.connection.base.Connection) + + +def unpatch(): + if getattr(pynamodb.connection.base, "_datadog_patch", False): + setattr(pynamodb.connection.base, "_datadog_patch", False) + unwrap(pynamodb.connection.base.Connection, "_make_api_call") + + +def patched_api_call(original_func, instance, args, kwargs): + + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return original_func(*args, **kwargs) + + with pin.tracer.trace( + "pynamodb.command", service=trace_utils.ext_service(pin, config.pynamodb, "pynamodb"), span_type=SpanTypes.HTTP + ) as span: + + span.set_tag(SPAN_MEASURED_KEY) + + try: + operation = get_argument_value(args, kwargs, 0, "operation_name") + span.resource = operation + + if args[1] and "TableName" in args[1]: + table_name = args[1]["TableName"] + span.set_tag("table_name", table_name) + span.resource = span.resource + " " + table_name + + except ArgumentError: + span.resource = "Unknown" + operation = None + + region_name = deep_getattr(instance, "client.meta.region_name") + + meta = { + "aws.agent": "pynamodb", + "aws.operation": operation, + "aws.region": region_name, + } + span.set_tags(meta) + + # set analytics sample rate + sample_rate = config.pynamodb.get_analytics_sample_rate(use_global_config=True) + + if sample_rate is not None: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate) + + result = original_func(*args, **kwargs) + + return result diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyodbc/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyodbc/__init__.py new file mode 100644 index 000000000..7e0dde198 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyodbc/__init__.py @@ -0,0 +1,65 @@ +""" +The pyodbc integration instruments the pyodbc library to trace pyodbc queries. + + +Enabling +~~~~~~~~ + +The integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(pyodbc=True) + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.pyodbc["service"] + + The service name reported by default for pyodbc spans. + + This option can also be set with the ``DD_PYODBC_SERVICE`` environment + variable. + + Default: ``"pyodbc"`` + +.. py:data:: ddtrace.config.pyodbc["trace_fetch_methods"] + + Whether or not to trace fetch methods. + + Can also configured via the ``DD_PYODBC_TRACE_FETCH_METHODS`` environment variable. + + Default: ``False`` + + +Instance Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +To configure the integration on an per-connection basis use the +``Pin`` API:: + + from ddtrace import Pin + import pyodbc + + # This will report a span with the default settings + db = pyodbc.connect("") + + # Use a pin to override the service name for the connection. + Pin.override(db, service='pyodbc-users') + + cursor = db.cursor() + cursor.execute("select * from users where id = 1") +""" +from ...utils.importlib import require_modules + + +required_modules = ["pyodbc"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + + __all__ = ["patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyodbc/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyodbc/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..5138a3dd7 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyodbc/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyodbc/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyodbc/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..3127d237b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyodbc/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyodbc/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyodbc/patch.py new file mode 100644 index 000000000..872a1df84 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyodbc/patch.py @@ -0,0 +1,55 @@ +import pyodbc + +from ... import Pin +from ... import config +from ...utils.formats import asbool +from ...utils.formats import get_env +from ..dbapi import TracedConnection +from ..dbapi import TracedCursor +from ..trace_utils import unwrap +from ..trace_utils import wrap + + +config._add( + "pyodbc", + dict( + _default_service="pyodbc", + trace_fetch_methods=asbool(get_env("pyodbc", "trace_fetch_methods", default=False)), + ), +) + + +def patch(): + if getattr(pyodbc, "_datadog_patch", False): + return + setattr(pyodbc, "_datadog_patch", True) + wrap("pyodbc", "connect", _connect) + + +def unpatch(): + if getattr(pyodbc, "_datadog_patch", False): + setattr(pyodbc, "_datadog_patch", False) + unwrap(pyodbc, "connect") + + +def _connect(func, instance, args, kwargs): + conn = func(*args, **kwargs) + return patch_conn(conn) + + +def patch_conn(conn): + pin = Pin(service=None, app="pyodbc") + wrapped = PyODBCTracedConnection(conn, pin=pin) + pin.onto(wrapped) + return wrapped + + +class PyODBCTracedCursor(TracedCursor): + pass + + +class PyODBCTracedConnection(TracedConnection): + def __init__(self, conn, pin=None, cursor_cls=None): + if not cursor_cls: + cursor_cls = PyODBCTracedCursor + super(PyODBCTracedConnection, self).__init__(conn, pin, config.pyodbc, cursor_cls=cursor_cls) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/__init__.py new file mode 100644 index 000000000..5038a837a --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/__init__.py @@ -0,0 +1,60 @@ +r"""To trace requests from a Pyramid application, trace your application +config:: + + + from pyramid.config import Configurator + from ddtrace.contrib.pyramid import trace_pyramid + + settings = { + 'datadog_trace_service' : 'my-web-app-name', + } + + config = Configurator(settings=settings) + trace_pyramid(config) + + # use your config as normal. + config.add_route('index', '/') + +Available settings are: + +* ``datadog_trace_service``: change the `pyramid` service name +* ``datadog_trace_enabled``: sets if the Tracer is enabled or not +* ``datadog_distributed_tracing``: set it to ``False`` to disable Distributed Tracing + +If you use the ``pyramid.tweens`` settings value to set the tweens for your +application, you need to add ``ddtrace.contrib.pyramid:trace_tween_factory`` +explicitly to the list. For example:: + + settings = { + 'datadog_trace_service' : 'my-web-app-name', + 'pyramid.tweens', 'your_tween_no_1\\nyour_tween_no_2\\nddtrace.contrib.pyramid:trace_tween_factory', + } + + config = Configurator(settings=settings) + trace_pyramid(config) + + # use your config as normal. + config.add_route('index', '/') + +:ref:`All HTTP tags ` are supported for this integration. + +""" + +from ...utils.importlib import require_modules + + +required_modules = ["pyramid"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .trace import includeme + from .trace import trace_pyramid + from .trace import trace_tween_factory + + __all__ = [ + "patch", + "trace_pyramid", + "trace_tween_factory", + "includeme", + ] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..601bef599 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/__pycache__/constants.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/__pycache__/constants.cpython-38.pyc new file mode 100644 index 000000000..c24cefbbd Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/__pycache__/constants.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..d02ca4475 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/__pycache__/trace.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/__pycache__/trace.cpython-38.pyc new file mode 100644 index 000000000..6c8d35ec7 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/__pycache__/trace.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/constants.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/constants.py new file mode 100644 index 000000000..4d96877cc --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/constants.py @@ -0,0 +1,6 @@ +SETTINGS_SERVICE = "datadog_trace_service" +SETTINGS_TRACER = "datadog_tracer" +SETTINGS_TRACE_ENABLED = "datadog_trace_enabled" +SETTINGS_DISTRIBUTED_TRACING = "datadog_distributed_tracing" +SETTINGS_ANALYTICS_ENABLED = "datadog_analytics_enabled" +SETTINGS_ANALYTICS_SAMPLE_RATE = "datadog_analytics_sample_rate" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/patch.py new file mode 100644 index 000000000..579d17935 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/patch.py @@ -0,0 +1,93 @@ +import os + +import pyramid.config + +from ddtrace import config +from ddtrace.vendor import wrapt + +from ...utils.formats import asbool +from ...utils.formats import get_env +from .constants import SETTINGS_ANALYTICS_ENABLED +from .constants import SETTINGS_ANALYTICS_SAMPLE_RATE +from .constants import SETTINGS_DISTRIBUTED_TRACING +from .constants import SETTINGS_SERVICE +from .trace import DD_TWEEN_NAME +from .trace import trace_pyramid + + +config._add( + "pyramid", + dict( + distributed_tracing=asbool(get_env("pyramid", "distributed_tracing", default=True)), + ), +) + +DD_PATCH = "_datadog_patch" + + +def patch(): + """ + Patch pyramid.config.Configurator + """ + if getattr(pyramid.config, DD_PATCH, False): + return + + setattr(pyramid.config, DD_PATCH, True) + _w = wrapt.wrap_function_wrapper + _w("pyramid.config", "Configurator.__init__", traced_init) + + +def traced_init(wrapped, instance, args, kwargs): + settings = kwargs.pop("settings", {}) + service = config._get_service(default="pyramid") + # DEV: integration-specific analytics flag can be not set but still enabled + # globally for web frameworks + old_analytics_enabled = get_env("pyramid", "analytics_enabled") + analytics_enabled = os.environ.get("DD_TRACE_PYRAMID_ANALYTICS_ENABLED", old_analytics_enabled) + if analytics_enabled is not None: + analytics_enabled = asbool(analytics_enabled) + # TODO: why is analytics sample rate a string or a bool here? + old_analytics_sample_rate = get_env("pyramid", "analytics_sample_rate", default=True) + analytics_sample_rate = os.environ.get("DD_TRACE_PYRAMID_ANALYTICS_SAMPLE_RATE", old_analytics_sample_rate) + trace_settings = { + SETTINGS_SERVICE: service, + SETTINGS_DISTRIBUTED_TRACING: config.pyramid.distributed_tracing, + SETTINGS_ANALYTICS_ENABLED: analytics_enabled, + SETTINGS_ANALYTICS_SAMPLE_RATE: analytics_sample_rate, + } + # Update over top of the defaults + # DEV: If we did `settings.update(trace_settings)` then we would only ever + # have the default values. + trace_settings.update(settings) + # If the tweens are explicitly set with 'pyramid.tweens', we need to + # explicitly set our tween too since `add_tween` will be ignored. + insert_tween_if_needed(trace_settings) + + # The original Configurator.__init__ looks up two levels to find the package + # name if it is not provided. This has to be replicated here since this patched + # call will occur at the same level in the call stack. + if not kwargs.get("package", None): + from pyramid.path import caller_package + + kwargs["package"] = caller_package(level=2) + + kwargs["settings"] = trace_settings + wrapped(*args, **kwargs) + trace_pyramid(instance) + + +def insert_tween_if_needed(settings): + tweens = settings.get("pyramid.tweens") + # If the list is empty, pyramid does not consider the tweens have been + # set explicitly. + # And if our tween is already there, nothing to do + if not tweens or not tweens.strip() or DD_TWEEN_NAME in tweens: + return + # pyramid.tweens.EXCVIEW is the name of built-in exception view provided by + # pyramid. We need our tween to be before it, otherwise unhandled + # exceptions will be caught before they reach our tween. + idx = tweens.find(pyramid.tweens.EXCVIEW) + if idx == -1: + settings["pyramid.tweens"] = tweens + "\n" + DD_TWEEN_NAME + else: + settings["pyramid.tweens"] = tweens[:idx] + DD_TWEEN_NAME + "\n" + tweens[idx:] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/trace.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/trace.py new file mode 100644 index 000000000..ffc81fae1 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pyramid/trace.py @@ -0,0 +1,123 @@ +from pyramid.httpexceptions import HTTPException +import pyramid.renderers +from pyramid.settings import asbool + +# project +import ddtrace +from ddtrace import config +from ddtrace.vendor import wrapt + +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...internal.logger import get_logger +from .constants import SETTINGS_ANALYTICS_ENABLED +from .constants import SETTINGS_ANALYTICS_SAMPLE_RATE +from .constants import SETTINGS_DISTRIBUTED_TRACING +from .constants import SETTINGS_SERVICE +from .constants import SETTINGS_TRACER +from .constants import SETTINGS_TRACE_ENABLED + + +log = get_logger(__name__) + +DD_TWEEN_NAME = "ddtrace.contrib.pyramid:trace_tween_factory" +DD_SPAN = "_datadog_span" + + +def trace_pyramid(config): + config.include("ddtrace.contrib.pyramid") + + +def includeme(config): + # Add our tween just before the default exception handler + config.add_tween(DD_TWEEN_NAME, over=pyramid.tweens.EXCVIEW) + # ensure we only patch the renderer once. + if not isinstance(pyramid.renderers.RendererHelper.render, wrapt.ObjectProxy): + wrapt.wrap_function_wrapper("pyramid.renderers", "RendererHelper.render", trace_render) + + +def trace_render(func, instance, args, kwargs): + # If the request is not traced, we do not trace + request = kwargs.get("request", {}) + if not request: + log.debug("No request passed to render, will not be traced") + return func(*args, **kwargs) + span = getattr(request, DD_SPAN, None) + if not span: + log.debug("No span found in request, will not be traced") + return func(*args, **kwargs) + + with span.tracer.trace("pyramid.render", span_type=SpanTypes.TEMPLATE) as span: + return func(*args, **kwargs) + + +def trace_tween_factory(handler, registry): + # configuration + settings = registry.settings + service = settings.get(SETTINGS_SERVICE) or "pyramid" + tracer = settings.get(SETTINGS_TRACER) or ddtrace.tracer + enabled = asbool(settings.get(SETTINGS_TRACE_ENABLED, tracer.enabled)) + distributed_tracing = asbool(settings.get(SETTINGS_DISTRIBUTED_TRACING, True)) + + if enabled: + # make a request tracing function + def trace_tween(request): + trace_utils.activate_distributed_headers( + tracer, int_config=config.pyramid, request_headers=request.headers, override=distributed_tracing + ) + + with tracer.trace("pyramid.request", service=service, resource="404", span_type=SpanTypes.WEB) as span: + span.set_tag(SPAN_MEASURED_KEY) + # Configure trace search sample rate + # DEV: pyramid is special case maintains separate configuration from config api + analytics_enabled = settings.get(SETTINGS_ANALYTICS_ENABLED) + + if (config.analytics_enabled and analytics_enabled is not False) or analytics_enabled is True: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, settings.get(SETTINGS_ANALYTICS_SAMPLE_RATE, True)) + + setattr(request, DD_SPAN, span) # used to find the tracer in templates + response = None + status = None + try: + response = handler(request) + except HTTPException as e: + # If the exception is a pyramid HTTPException, + # that's still valuable information that isn't necessarily + # a 500. For instance, HTTPFound is a 302. + # As described in docs, Pyramid exceptions are all valid + # response types + response = e + raise + except BaseException: + status = 500 + raise + finally: + # set request tags + if request.matched_route: + span.resource = "{} {}".format(request.method, request.matched_route.name) + span.set_tag("pyramid.route.name", request.matched_route.name) + # set response tags + if response: + status = response.status_code + response_headers = response.headers + else: + response_headers = None + + trace_utils.set_http_meta( + span, + config.pyramid, + method=request.method, + url=request.path_url, + status_code=status, + query=request.query_string, + request_headers=request.headers, + response_headers=response_headers, + ) + return response + + return trace_tween + + # if timing support is not enabled, return the original handler + return handler diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/__init__.py new file mode 100644 index 000000000..c62ffd74f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/__init__.py @@ -0,0 +1,54 @@ +""" +The pytest integration traces test executions. + +Enabling +~~~~~~~~ + +Enable traced execution of tests using ``pytest`` runner by +running ``pytest --ddtrace`` or by modifying any configuration +file read by pytest (``pytest.ini``, ``setup.cfg``, ...):: + + [pytest] + ddtrace = 1 + +You can enable all integrations by using the ``--ddtrace-patch-all`` option or by adding this to your configuration:: + + [pytest] + ddtrace-patch-all = 1 + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.pytest["service"] + + The service name reported by default for pytest traces. + + This option can also be set with the integration specific ``DD_PYTEST_SERVICE`` environment + variable, or more generally with the `DD_SERVICE` environment variable. + + Default: Name of the repository being tested, otherwise ``"pytest"`` if the repository name cannot be found. + + +.. py:data:: ddtrace.config.pytest["operation_name"] + + The operation name reported by default for pytest traces. + + This option can also be set with the ``DD_PYTEST_OPERATION_NAME`` environment + variable. + + Default: ``"pytest.test"`` +""" + +from ddtrace import config + +from ...utils.formats import get_env + + +# pytest default settings +config._add( + "pytest", + dict( + _default_service="pytest", + operation_name=get_env("pytest", "operation_name", default="pytest.test"), + ), +) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..7ad3953a6 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/__pycache__/constants.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/__pycache__/constants.cpython-38.pyc new file mode 100644 index 000000000..f633c4f1d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/__pycache__/constants.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/__pycache__/plugin.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/__pycache__/plugin.cpython-38.pyc new file mode 100644 index 000000000..21ede6f79 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/__pycache__/plugin.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/constants.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/constants.py new file mode 100644 index 000000000..b82144712 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/constants.py @@ -0,0 +1,4 @@ +FRAMEWORK = "pytest" +KIND = "test" + +HELP_MSG = "Enable tracing of pytest functions." diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/plugin.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/plugin.py new file mode 100644 index 000000000..7b155f816 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/pytest/plugin.py @@ -0,0 +1,204 @@ +from doctest import DocTest +import json +from typing import Dict + +import pytest + +import ddtrace +from ddtrace.constants import AUTO_KEEP +from ddtrace.constants import SPAN_KIND +from ddtrace.contrib.pytest.constants import FRAMEWORK +from ddtrace.contrib.pytest.constants import HELP_MSG +from ddtrace.contrib.pytest.constants import KIND +from ddtrace.contrib.trace_utils import int_service +from ddtrace.ext import SpanTypes +from ddtrace.ext import ci +from ddtrace.ext import test +from ddtrace.internal import compat +from ddtrace.internal.logger import get_logger +from ddtrace.pin import Pin + + +PATCH_ALL_HELP_MSG = "Call ddtrace.patch_all before running tests." +log = get_logger(__name__) + + +def is_enabled(config): + """Check if the ddtrace plugin is enabled.""" + return config.getoption("ddtrace") or config.getini("ddtrace") + + +def _extract_span(item): + """Extract span from `pytest.Item` instance.""" + return getattr(item, "_datadog_span", None) + + +def _store_span(item, span): + """Store span at `pytest.Item` instance.""" + setattr(item, "_datadog_span", span) + + +def _extract_repository_name(repository_url): + # type: (str) -> str + """Extract repository name from repository url.""" + try: + return compat.parse.urlparse(repository_url).path.rstrip(".git").rpartition("/")[-1] + except ValueError: + # In case of parsing error, default to repository url + log.warning("Repository name cannot be parsed from repository_url: %s", repository_url) + return repository_url + + +def pytest_addoption(parser): + """Add ddtrace options.""" + group = parser.getgroup("ddtrace") + + group._addoption( + "--ddtrace", + action="store_true", + dest="ddtrace", + default=False, + help=HELP_MSG, + ) + + group._addoption( + "--ddtrace-patch-all", + action="store_true", + dest="ddtrace-patch-all", + default=False, + help=PATCH_ALL_HELP_MSG, + ) + + parser.addini("ddtrace", HELP_MSG, type="bool") + parser.addini("ddtrace-patch-all", PATCH_ALL_HELP_MSG, type="bool") + + +def pytest_configure(config): + config.addinivalue_line("markers", "dd_tags(**kwargs): add tags to current span") + if is_enabled(config): + ci_tags = ci.tags() + if ci_tags.get(ci.git.REPOSITORY_URL, None) and int_service(None, ddtrace.config.pytest) == "pytest": + repository_name = _extract_repository_name(ci_tags[ci.git.REPOSITORY_URL]) + ddtrace.config.pytest["service"] = repository_name + Pin(tags=ci_tags, _config=ddtrace.config.pytest).onto(config) + + +def pytest_sessionfinish(session, exitstatus): + """Flush open tracer.""" + pin = Pin.get_from(session.config) + if pin is not None: + pin.tracer.shutdown() + + +@pytest.fixture(scope="function") +def ddspan(request): + pin = Pin.get_from(request.config) + if pin: + return _extract_span(request.node) + + +@pytest.fixture(scope="session", autouse=True) +def patch_all(request): + if request.config.getoption("ddtrace-patch-all") or request.config.getini("ddtrace-patch-all"): + ddtrace.patch_all() + + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_protocol(item, nextitem): + pin = Pin.get_from(item.config) + if pin is None: + yield + return + with pin.tracer.trace( + ddtrace.config.pytest.operation_name, + service=int_service(pin, ddtrace.config.pytest), + resource=item.nodeid, + span_type=SpanTypes.TEST.value, + ) as span: + span.context.dd_origin = ci.CI_APP_TEST_ORIGIN + span.context.sampling_priority = AUTO_KEEP + span.set_tags(pin.tags) + span.set_tag(SPAN_KIND, KIND) + span.set_tag(test.FRAMEWORK, FRAMEWORK) + span.set_tag(test.NAME, item.name) + if hasattr(item, "module"): + span.set_tag(test.SUITE, item.module.__name__) + elif hasattr(item, "dtest") and isinstance(item.dtest, DocTest): + span.set_tag(test.SUITE, item.dtest.globs["__name__"]) + span.set_tag(test.TYPE, SpanTypes.TEST.value) + + span.set_tag(test.FRAMEWORK_VERSION, pytest.__version__) + + # We preemptively set FAIL as a status, because if pytest_runtest_makereport is not called + # (where the actual test status is set), it means there was a pytest error + span.set_tag(test.STATUS, test.Status.FAIL.value) + + # Parameterized test cases will have a `callspec` attribute attached to the pytest Item object. + # Pytest docs: https://docs.pytest.org/en/6.2.x/reference.html#pytest.Function + if getattr(item, "callspec", None): + parameters = {"arguments": {}, "metadata": {}} # type: Dict[str, Dict[str, str]] + for param_name, param_val in item.callspec.params.items(): + try: + parameters["arguments"][param_name] = repr(param_val) + except Exception: + parameters["arguments"][param_name] = "Could not encode" + log.warning("Failed to encode %r", param_name, exc_info=True) + span.set_tag(test.PARAMETERS, json.dumps(parameters)) + + markers = [marker.kwargs for marker in item.iter_markers(name="dd_tags")] + for tags in markers: + span.set_tags(tags) + _store_span(item, span) + + yield + + +def _extract_reason(call): + if call.excinfo is not None: + return call.excinfo.value + + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_makereport(item, call): + """Store outcome for tracing.""" + outcome = yield + + span = _extract_span(item) + if span is None: + return + + is_setup_or_teardown = call.when == "setup" or call.when == "teardown" + has_exception = call.excinfo is not None + + if is_setup_or_teardown and not has_exception: + return + + result = outcome.get_result() + xfail = hasattr(result, "wasxfail") or "xfail" in result.keywords + has_skip_keyword = any(x in result.keywords for x in ["skip", "skipif", "skipped"]) + + if result.skipped: + if xfail and not has_skip_keyword: + # XFail tests that fail are recorded skipped by pytest, should be passed instead + span.set_tag(test.RESULT, test.Status.XFAIL.value) + span.set_tag(test.XFAIL_REASON, result.wasxfail) + span.set_tag(test.STATUS, test.Status.PASS.value) + else: + span.set_tag(test.STATUS, test.Status.SKIP.value) + reason = _extract_reason(call) + if reason is not None: + span.set_tag(test.SKIP_REASON, reason) + elif result.passed: + span.set_tag(test.STATUS, test.Status.PASS.value) + if xfail and not has_skip_keyword: + # XPass (strict=False) are recorded passed by pytest + span.set_tag(test.XFAIL_REASON, getattr(result, "wasxfail", "XFail")) + span.set_tag(test.RESULT, test.Status.XPASS.value) + else: + if xfail and not has_skip_keyword: + # XPass (strict=True) are recorded failed by pytest, longrepr contains reason + span.set_tag(test.XFAIL_REASON, result.longrepr) + span.set_tag(test.RESULT, test.Status.XPASS.value) + span.set_tag(test.STATUS, test.Status.FAIL.value) + if call.excinfo: + span.set_exc_info(call.excinfo.type, call.excinfo.value, call.excinfo.tb) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/__init__.py new file mode 100644 index 000000000..8b157b0e4 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/__init__.py @@ -0,0 +1,59 @@ +""" +The redis integration traces redis requests. + + +Enabling +~~~~~~~~ + +The redis integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(redis=True) + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.redis["service"] + + The service name reported by default for redis traces. + + This option can also be set with the ``DD_REDIS_SERVICE`` environment + variable. + + Default: ``"redis"`` + + +Instance Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +To configure particular redis instances use the :ref:`Pin` API:: + + import redis + from ddtrace import Pin + + client = redis.StrictRedis(host="localhost", port=6379) + + # Override service name for this instance + Pin.override(client, service="my-custom-queue") + + # Traces reported for this client will now have "my-custom-queue" + # as the service name. + client.get("my-key") +""" + +from ...utils.importlib import require_modules + + +required_modules = ["redis", "redis.client"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .tracers import get_traced_redis + from .tracers import get_traced_redis_from + + __all__ = ["get_traced_redis", "get_traced_redis_from", "patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..a1804507f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..210d66f9b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/__pycache__/tracers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/__pycache__/tracers.cpython-38.pyc new file mode 100644 index 000000000..adf207aa8 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/__pycache__/tracers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/__pycache__/util.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/__pycache__/util.cpython-38.pyc new file mode 100644 index 000000000..343c31b53 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/__pycache__/util.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/patch.py new file mode 100644 index 000000000..84badc854 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/patch.py @@ -0,0 +1,123 @@ +import redis + +from ddtrace import config +from ddtrace.vendor import wrapt + +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import redis as redisx +from ...pin import Pin +from ...utils.wrappers import unwrap +from .util import _extract_conn_tags +from .util import format_command_args + + +config._add("redis", dict(_default_service="redis")) + + +def patch(): + """Patch the instrumented methods + + This duplicated doesn't look nice. The nicer alternative is to use an ObjectProxy on top + of Redis and StrictRedis. However, it means that any "import redis.Redis" won't be instrumented. + """ + if getattr(redis, "_datadog_patch", False): + return + setattr(redis, "_datadog_patch", True) + + _w = wrapt.wrap_function_wrapper + + if redis.VERSION < (3, 0, 0): + _w("redis", "StrictRedis.execute_command", traced_execute_command) + _w("redis", "StrictRedis.pipeline", traced_pipeline) + _w("redis", "Redis.pipeline", traced_pipeline) + _w("redis.client", "BasePipeline.execute", traced_execute_pipeline) + _w("redis.client", "BasePipeline.immediate_execute_command", traced_execute_command) + else: + _w("redis", "Redis.execute_command", traced_execute_command) + _w("redis", "Redis.pipeline", traced_pipeline) + _w("redis.client", "Pipeline.execute", traced_execute_pipeline) + _w("redis.client", "Pipeline.immediate_execute_command", traced_execute_command) + Pin(service=None, app=redisx.APP).onto(redis.StrictRedis) + + +def unpatch(): + if getattr(redis, "_datadog_patch", False): + setattr(redis, "_datadog_patch", False) + + if redis.VERSION < (3, 0, 0): + unwrap(redis.StrictRedis, "execute_command") + unwrap(redis.StrictRedis, "pipeline") + unwrap(redis.Redis, "pipeline") + unwrap(redis.client.BasePipeline, "execute") + unwrap(redis.client.BasePipeline, "immediate_execute_command") + else: + unwrap(redis.Redis, "execute_command") + unwrap(redis.Redis, "pipeline") + unwrap(redis.client.Pipeline, "execute") + unwrap(redis.client.Pipeline, "immediate_execute_command") + + +# +# tracing functions +# +def traced_execute_command(func, instance, args, kwargs): + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + with pin.tracer.trace( + redisx.CMD, service=trace_utils.ext_service(pin, config.redis, pin), span_type=SpanTypes.REDIS + ) as s: + s.set_tag(SPAN_MEASURED_KEY) + query = format_command_args(args) + s.resource = query + s.set_tag(redisx.RAWCMD, query) + if pin.tags: + s.set_tags(pin.tags) + s.set_tags(_get_tags(instance)) + s.set_metric(redisx.ARGS_LEN, len(args)) + # set analytics sample rate if enabled + s.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.redis.get_analytics_sample_rate()) + # run the command + return func(*args, **kwargs) + + +def traced_pipeline(func, instance, args, kwargs): + pipeline = func(*args, **kwargs) + pin = Pin.get_from(instance) + if pin: + pin.onto(pipeline) + return pipeline + + +def traced_execute_pipeline(func, instance, args, kwargs): + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + # FIXME[matt] done in the agent. worth it? + cmds = [format_command_args(c) for c, _ in instance.command_stack] + resource = "\n".join(cmds) + tracer = pin.tracer + with tracer.trace( + redisx.CMD, + resource=resource, + service=trace_utils.ext_service(pin, config.redis), + span_type=SpanTypes.REDIS, + ) as s: + s.set_tag(SPAN_MEASURED_KEY) + s.set_tag(redisx.RAWCMD, resource) + s.set_tags(_get_tags(instance)) + s.set_metric(redisx.PIPELINE_LEN, len(instance.command_stack)) + + # set analytics sample rate if enabled + s.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.redis.get_analytics_sample_rate()) + + return func(*args, **kwargs) + + +def _get_tags(conn): + return _extract_conn_tags(conn.connection_pool.connection_kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/tracers.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/tracers.py new file mode 100644 index 000000000..de812fd58 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/tracers.py @@ -0,0 +1,20 @@ +from redis import StrictRedis + +from ...utils.deprecation import deprecated + + +DEFAULT_SERVICE = "redis" + + +@deprecated(message="Use patching instead (see the docs).", version="1.0.0") +def get_traced_redis(ddtracer, service=DEFAULT_SERVICE, meta=None): + return _get_traced_redis(ddtracer, StrictRedis, service, meta) + + +@deprecated(message="Use patching instead (see the docs).", version="1.0.0") +def get_traced_redis_from(ddtracer, baseclass, service=DEFAULT_SERVICE, meta=None): + return _get_traced_redis(ddtracer, baseclass, service, meta) + + +def _get_traced_redis(ddtracer, baseclass, service, meta): + return baseclass diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/util.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/util.py new file mode 100644 index 000000000..6796ba4d9 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/redis/util.py @@ -0,0 +1,54 @@ +""" +Some utils used by the dogtrace redis integration +""" +from ...ext import net +from ...ext import redis as redisx +from ...internal.compat import stringify + + +VALUE_PLACEHOLDER = "?" +VALUE_MAX_LEN = 100 +VALUE_TOO_LONG_MARK = "..." +CMD_MAX_LEN = 1000 + + +def _extract_conn_tags(conn_kwargs): + """Transform redis conn info into dogtrace metas""" + try: + return { + net.TARGET_HOST: conn_kwargs["host"], + net.TARGET_PORT: conn_kwargs["port"], + redisx.DB: conn_kwargs["db"] or 0, + } + except Exception: + return {} + + +def format_command_args(args): + """Format a command by removing unwanted values + + Restrict what we keep from the values sent (with a SET, HGET, LPUSH, ...): + - Skip binary content + - Truncate + """ + length = 0 + out = [] + for arg in args: + try: + cmd = stringify(arg) + + if len(cmd) > VALUE_MAX_LEN: + cmd = cmd[:VALUE_MAX_LEN] + VALUE_TOO_LONG_MARK + + if length + len(cmd) > CMD_MAX_LEN: + prefix = cmd[: CMD_MAX_LEN - length] + out.append("%s%s" % (prefix, VALUE_TOO_LONG_MARK)) + break + + out.append(cmd) + length += len(cmd) + except Exception: + out.append(VALUE_PLACEHOLDER) + break + + return " ".join(out) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rediscluster/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rediscluster/__init__.py new file mode 100644 index 000000000..78b4da70d --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rediscluster/__init__.py @@ -0,0 +1,29 @@ +"""Instrument rediscluster to report Redis Cluster queries. + +``patch_all`` will automatically patch your Redis Cluster client to make it work. +:: + + from ddtrace import Pin, patch + import rediscluster + + # If not patched yet, you can patch redis specifically + patch(rediscluster=True) + + # This will report a span with the default settings + client = rediscluster.StrictRedisCluster(startup_nodes=[{'host':'localhost', 'port':'7000'}]) + client.get('my-key') + + # Use a pin to specify metadata related to this client + Pin.override(client, service='redis-queue') +""" + +from ...utils.importlib import require_modules + + +required_modules = ["rediscluster", "rediscluster.client"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + + __all__ = ["patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rediscluster/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rediscluster/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..3bdf43eba Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rediscluster/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rediscluster/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rediscluster/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..1cdca6cc2 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rediscluster/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rediscluster/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rediscluster/patch.py new file mode 100644 index 000000000..6e546045a --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rediscluster/patch.py @@ -0,0 +1,77 @@ +# 3p +import rediscluster + +# project +from ddtrace import config +from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY +from ddtrace.constants import SPAN_MEASURED_KEY +from ddtrace.contrib.redis.patch import traced_execute_command +from ddtrace.contrib.redis.patch import traced_pipeline +from ddtrace.contrib.redis.util import format_command_args +from ddtrace.ext import SpanTypes +from ddtrace.ext import redis as redisx +from ddtrace.pin import Pin +from ddtrace.utils.wrappers import unwrap +from ddtrace.vendor import wrapt + + +# DEV: In `2.0.0` `__version__` is a string and `VERSION` is a tuple, +# but in `1.x.x` `__version__` is a tuple annd `VERSION` does not exist +REDISCLUSTER_VERSION = getattr(rediscluster, "VERSION", rediscluster.__version__) + + +def patch(): + """Patch the instrumented methods""" + if getattr(rediscluster, "_datadog_patch", False): + return + setattr(rediscluster, "_datadog_patch", True) + + _w = wrapt.wrap_function_wrapper + if REDISCLUSTER_VERSION >= (2, 0, 0): + _w("rediscluster", "client.RedisCluster.execute_command", traced_execute_command) + _w("rediscluster", "client.RedisCluster.pipeline", traced_pipeline) + _w("rediscluster", "pipeline.ClusterPipeline.execute", traced_execute_pipeline) + Pin(service=redisx.DEFAULT_SERVICE, app=redisx.APP).onto(rediscluster.RedisCluster) + else: + _w("rediscluster", "StrictRedisCluster.execute_command", traced_execute_command) + _w("rediscluster", "StrictRedisCluster.pipeline", traced_pipeline) + _w("rediscluster", "StrictClusterPipeline.execute", traced_execute_pipeline) + Pin(service=redisx.DEFAULT_SERVICE, app=redisx.APP).onto(rediscluster.StrictRedisCluster) + + +def unpatch(): + if getattr(rediscluster, "_datadog_patch", False): + setattr(rediscluster, "_datadog_patch", False) + + if REDISCLUSTER_VERSION >= (2, 0, 0): + unwrap(rediscluster.client.RedisCluster, "execute_command") + unwrap(rediscluster.client.RedisCluster, "pipeline") + unwrap(rediscluster.pipeline.ClusterPipeline, "execute") + else: + unwrap(rediscluster.StrictRedisCluster, "execute_command") + unwrap(rediscluster.StrictRedisCluster, "pipeline") + unwrap(rediscluster.StrictClusterPipeline, "execute") + + +# +# tracing functions +# + + +def traced_execute_pipeline(func, instance, args, kwargs): + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + cmds = [format_command_args(c.args) for c in instance.command_stack] + resource = "\n".join(cmds) + tracer = pin.tracer + with tracer.trace(redisx.CMD, resource=resource, service=pin.service, span_type=SpanTypes.REDIS) as s: + s.set_tag(SPAN_MEASURED_KEY) + s.set_tag(redisx.RAWCMD, resource) + s.set_metric(redisx.PIPELINE_LEN, len(instance.command_stack)) + + # set analytics sample rate if enabled + s.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.rediscluster.get_analytics_sample_rate()) + + return func(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__init__.py new file mode 100644 index 000000000..8000bce32 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__init__.py @@ -0,0 +1,89 @@ +""" +The ``requests`` integration traces all HTTP requests made with the ``requests`` +library. + +The default service name used is `requests` but it can be configured to match +the services that the specific requests are made to. + +Enabling +~~~~~~~~ + +The requests integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(requests=True) + + # use requests like usual + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.requests['service'] + + The service name reported by default for requests queries. This value will + be overridden by an instance override or if the split_by_domain setting is + enabled. + + This option can also be set with the ``DD_REQUESTS_SERVICE`` environment + variable. + + Default: ``"requests"`` + + +.. py:data:: ddtrace.config.requests['distributed_tracing'] + + Whether or not to parse distributed tracing headers. + + Default: ``True`` + + +.. py:data:: ddtrace.config.requests['trace_query_string'] + + Whether or not to include the query string as a tag. + + Default: ``False`` + + +.. py:data:: ddtrace.config.requests['split_by_domain'] + + Whether or not to use the domain name of requests as the service name. This + setting can be overridden with session overrides (described in the Instance + Configuration section). + + Default: ``False`` + + +Instance Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +To set configuration options for all requests made with a ``requests.Session`` object +use the config API:: + + from ddtrace import config + from requests import Session + + session = Session() + cfg = config.get_from(session) + cfg['service_name'] = 'auth-api' + cfg['distributed_tracing'] = False +""" +from ...utils.importlib import require_modules + + +required_modules = ["requests"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + from .session import TracedSession + + __all__ = [ + "patch", + "unpatch", + "TracedSession", + ] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..217fbbac4 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/connection.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/connection.cpython-38.pyc new file mode 100644 index 000000000..94c6dc1bd Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/connection.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/constants.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/constants.cpython-38.pyc new file mode 100644 index 000000000..5b2278e22 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/constants.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/legacy.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/legacy.cpython-38.pyc new file mode 100644 index 000000000..afe42b375 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/legacy.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..3fbde5692 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/session.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/session.cpython-38.pyc new file mode 100644 index 000000000..d55457809 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/__pycache__/session.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/connection.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/connection.py new file mode 100644 index 000000000..40c0b5065 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/connection.py @@ -0,0 +1,125 @@ +from typing import Optional + +import ddtrace +from ddtrace import config + +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...internal.logger import get_logger +from ...propagation.http import HTTPPropagator +from ...utils import get_argument_value + + +log = get_logger(__name__) + + +def _extract_hostname(uri): + # type: (str) -> str + end = len(uri) + j = uri.rfind("#", 0, end) + if j != -1: + end = j + j = uri.rfind("&", 0, end) + if j != -1: + end = j + + start = uri.find("://", 0, end) + 3 + i = uri.find("@", start, end) + 1 + if i != 0: + start = i + j = uri.find("/", start, end) + if j != -1: + end = j + + return uri[start:end] + + +def _extract_query_string(uri): + # type: (str) -> Optional[str] + start = uri.find("?") + 1 + if start == 0: + return None + + end = len(uri) + j = uri.rfind("#", 0, end) + if j != -1: + end = j + + if end <= start: + return None + + return uri[start:end] + + +def _wrap_send(func, instance, args, kwargs): + """Trace the `Session.send` instance method""" + # TODO[manu]: we already offer a way to provide the Global Tracer + # and is ddtrace.tracer; it's used only inside our tests and can + # be easily changed by providing a TracingTestCase that sets common + # tracing functionalities. + tracer = getattr(instance, "datadog_tracer", ddtrace.tracer) + + # skip if tracing is not enabled + if not tracer.enabled: + return func(*args, **kwargs) + + request = get_argument_value(args, kwargs, 0, "request") + if not request: + return func(*args, **kwargs) + + url = request.url + hostname = _extract_hostname(url) + + cfg = config.get_from(instance) + service = None + if cfg["split_by_domain"] and hostname: + service = hostname + if service is None: + service = cfg.get("service", None) + if service is None: + service = cfg.get("service_name", None) + if service is None: + service = trace_utils.ext_service(None, config.requests) + + with tracer.trace("requests.request", service=service, span_type=SpanTypes.HTTP) as span: + span.set_tag(SPAN_MEASURED_KEY) + + # Configure trace search sample rate + # DEV: analytics enabled on per-session basis + cfg = config.get_from(instance) + analytics_enabled = cfg.get("analytics_enabled") + if analytics_enabled: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, cfg.get("analytics_sample_rate", True)) + + # propagate distributed tracing headers + if cfg.get("distributed_tracing"): + HTTPPropagator.inject(span.context, request.headers) + + response = response_headers = None + try: + response = func(*args, **kwargs) + return response + finally: + try: + status = None + if response is not None: + status = response.status_code + # Storing response headers in the span. + # Note that response.headers is not a dict, but an iterable + # requests custom structure, that we convert to a dict + response_headers = dict(getattr(response, "headers", {})) + + trace_utils.set_http_meta( + span, + config.requests, + request_headers=request.headers, + response_headers=response_headers, + method=request.method.upper(), + url=request.url, + status_code=status, + query=_extract_query_string(url), + ) + except Exception: + log.debug("requests: error adding tags", exc_info=True) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/constants.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/constants.py new file mode 100644 index 000000000..c3f5eaca5 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/constants.py @@ -0,0 +1 @@ +DEFAULT_SERVICE = "requests" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/legacy.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/legacy.py new file mode 100644 index 000000000..6a7b4e720 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/legacy.py @@ -0,0 +1,31 @@ +# [Deprecation]: this module contains deprecated functions +# that will be removed in newer versions of the Tracer. +from ddtrace import config + +from ...utils.deprecation import deprecation + + +def _distributed_tracing(self): + """Deprecated: this method has been deprecated in favor of + the configuration system. It will be removed in newer versions + of the Tracer. + """ + deprecation( + name="client.distributed_tracing", + message="Use the configuration object instead `config.get_from(client)['distributed_tracing'`", + version="1.0.0", + ) + return config.get_from(self)["distributed_tracing"] + + +def _distributed_tracing_setter(self, value): + """Deprecated: this method has been deprecated in favor of + the configuration system. It will be removed in newer versions + of the Tracer. + """ + deprecation( + name="client.distributed_tracing", + message="Use the configuration object instead `config.get_from(client)['distributed_tracing'] = value`", + version="1.0.0", + ) + config.get_from(self)["distributed_tracing"] = value diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/patch.py new file mode 100644 index 000000000..298c68fb3 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/patch.py @@ -0,0 +1,49 @@ +import requests + +from ddtrace import config +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from ...pin import Pin +from ...utils.formats import asbool +from ...utils.formats import get_env +from ...utils.wrappers import unwrap as _u +from .connection import _wrap_send +from .legacy import _distributed_tracing +from .legacy import _distributed_tracing_setter + + +# requests default settings +config._add( + "requests", + { + "distributed_tracing": asbool(get_env("requests", "distributed_tracing", default=True)), + "split_by_domain": asbool(get_env("requests", "split_by_domain", default=False)), + "_default_service": "requests", + }, +) + + +def patch(): + """Activate http calls tracing""" + if getattr(requests, "__datadog_patch", False): + return + setattr(requests, "__datadog_patch", True) + + _w("requests", "Session.send", _wrap_send) + Pin(app="requests", _config=config.requests).onto(requests.Session) + + # [Backward compatibility]: `session.distributed_tracing` should point and + # update the `Pin` configuration instead. This block adds a property so that + # old implementations work as expected + fn = property(_distributed_tracing) + fn = fn.setter(_distributed_tracing_setter) + requests.Session.distributed_tracing = fn + + +def unpatch(): + """Disable traced sessions""" + if not getattr(requests, "__datadog_patch", False): + return + setattr(requests, "__datadog_patch", False) + + _u(requests.Session, "send") diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/session.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/session.py new file mode 100644 index 000000000..1e603fb39 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/requests/session.py @@ -0,0 +1,21 @@ +import requests + +from ddtrace import Pin +from ddtrace import config +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from .connection import _wrap_send + + +class TracedSession(requests.Session): + """TracedSession is a requests' Session that is already traced. + You can use it if you want a finer grained control for your + HTTP clients. + """ + + pass + + +# always patch our `TracedSession` when imported +_w(TracedSession, "send", _wrap_send) +Pin(_config=config.requests).onto(TracedSession) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rq/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rq/__init__.py new file mode 100644 index 000000000..19ae472c2 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rq/__init__.py @@ -0,0 +1,251 @@ +""" +The RQ__ integration will trace your jobs. + + +Usage +~~~~~ + +The rq integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(rq=True) + + +Worker Usage +~~~~~~~~~~~~ + +``ddtrace-run`` can be used to easily trace your workers:: + + DD_SERVICE=myworker ddtrace-run rq worker + + + +Instance Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +To override the service name for a queue:: + + from ddtrace import Pin + + connection = redis.Redis() + queue = rq.Queue(connection=connection) + Pin.override(queue, service="custom_queue_service") + + +To override the service name for a particular worker:: + + worker = rq.SimpleWorker([queue], connection=queue.connection) + Pin.override(worker, service="custom_worker_service") + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.rq['distributed_tracing_enabled'] +.. py:data:: ddtrace.config.rq_worker['distributed_tracing_enabled'] + + If ``True`` the integration will connect the traces sent between the enqueuer + and the RQ worker. + + This option can also be set with the ``DD_RQ_DISTRIBUTED_TRACING_ENABLED`` + environment variable on either the enqueuer or worker applications. + + Default: ``True`` + +.. py:data:: ddtrace.config.rq['service'] + + The service name reported by default for RQ spans from the app. + + This option can also be set with the ``DD_SERVICE`` or ``DD_RQ_SERVICE`` + environment variables. + + Default: ``rq`` + +.. py:data:: ddtrace.config.rq_worker['service'] + + The service name reported by default for RQ spans from workers. + + This option can also be set with the ``DD_SERVICE`` environment + variable. + + Default: ``rq-worker`` + +.. __: https://python-rq.org/ + +""" +import os + +from ddtrace import Pin +from ddtrace import config + +from .. import trace_utils +from ...ext import SpanTypes +from ...propagation.http import HTTPPropagator +from ...utils import get_argument_value +from ...utils.formats import asbool + + +__all__ = [ + "patch", + "unpatch", +] + + +config._add( + "rq", + dict( + distributed_tracing_enabled=asbool(os.environ.get("DD_RQ_DISTRIBUTED_TRACING_ENABLED", True)), + _default_service="rq", + ), +) + +config._add( + "rq_worker", + dict( + distributed_tracing_enabled=asbool(os.environ.get("DD_RQ_DISTRIBUTED_TRACING_ENABLED", True)), + _default_service="rq-worker", + ), +) + + +@trace_utils.with_traced_module +def traced_queue_enqueue_job(rq, pin, func, instance, args, kwargs): + job = get_argument_value(args, kwargs, 0, "f") + + func_name = job.func_name + job_inst = job.instance + job_inst_str = "%s.%s" % (job_inst.__module__, job_inst.__class__.__name__) if job_inst else "" + + if job_inst_str: + resource = "%s.%s" % (job_inst_str, func_name) + else: + resource = func_name + + with pin.tracer.trace( + "rq.queue.enqueue_job", + service=trace_utils.int_service(pin, config.rq), + resource=resource, + span_type=SpanTypes.WORKER, + ) as span: + span._set_str_tag("queue.name", instance.name) + span.set_tag("job.id", job.get_id()) + span._set_str_tag("job.func_name", job.func_name) + + # If the queue is_async then add distributed tracing headers to the job + if instance.is_async and config.rq.distributed_tracing_enabled: + HTTPPropagator.inject(span.context, job.meta) + return func(*args, **kwargs) + + +@trace_utils.with_traced_module +def traced_queue_fetch_job(rq, pin, func, instance, args, kwargs): + with pin.tracer.trace("rq.queue.fetch_job", service=trace_utils.int_service(pin, config.rq)) as span: + job_id = get_argument_value(args, kwargs, 0, "job_id") + span.set_tag("job.id", job_id) + return func(*args, **kwargs) + + +@trace_utils.with_traced_module +def traced_perform_job(rq, pin, func, instance, args, kwargs): + """Trace rq.Worker.perform_job""" + # `perform_job` is executed in a freshly forked, short-lived instance + job = get_argument_value(args, kwargs, 0, "job") + + if config.rq_worker.distributed_tracing_enabled: + ctx = HTTPPropagator.extract(job.meta) + if ctx.trace_id: + pin.tracer.context_provider.activate(ctx) + + try: + with pin.tracer.trace( + "rq.worker.perform_job", + service=trace_utils.int_service(pin, config.rq_worker), + span_type=SpanTypes.WORKER, + resource=job.func_name, + ) as span: + span.set_tag("job.id", job.get_id()) + try: + return func(*args, **kwargs) + finally: + span.set_tag("job.status", job.get_status()) + span.set_tag("job.origin", job.origin) + if job.is_failed: + span.error = 1 + finally: + # Force flush to agent since the process `os.exit()`s + # immediately after this method returns + pin.tracer.writer.flush_queue() + + +@trace_utils.with_traced_module +def traced_job_perform(rq, pin, func, instance, args, kwargs): + """Trace rq.Job.perform(...)""" + job = instance + + # Inherit the service name from whatever parent exists. + # eg. in a worker, a perform_job parent span will exist with the worker + # service. + with pin.tracer.trace("rq.job.perform", resource=job.func_name) as span: + span.set_tag("job.id", job.get_id()) + return func(*args, **kwargs) + + +@trace_utils.with_traced_module +def traced_job_fetch_many(rq, pin, func, instance, args, kwargs): + """Trace rq.Job.fetch_many(...)""" + with pin.tracer.trace("rq.job.fetch_many", service=trace_utils.ext_service(pin, config.rq_worker)) as span: + job_ids = get_argument_value(args, kwargs, 0, "job_ids") + span.set_tag("job_ids", job_ids) + return func(*args, **kwargs) + + +def patch(): + # Avoid importing rq at the module level, eventually will be an import hook + import rq + + if getattr(rq, "_datadog_patch", False): + return + + Pin().onto(rq) + + # Patch rq.job.Job + Pin().onto(rq.job.Job) + trace_utils.wrap(rq.job, "Job.perform", traced_job_perform(rq.job.Job)) + + # Patch rq.queue.Queue + Pin().onto(rq.queue.Queue) + trace_utils.wrap("rq.queue", "Queue.enqueue_job", traced_queue_enqueue_job(rq)) + trace_utils.wrap("rq.queue", "Queue.fetch_job", traced_queue_fetch_job(rq)) + + # Patch rq.worker.Worker + Pin().onto(rq.worker.Worker) + trace_utils.wrap(rq.worker, "Worker.perform_job", traced_perform_job(rq)) + + setattr(rq, "_datadog_patch", True) + + +def unpatch(): + import rq + + if not getattr(rq, "_datadog_patch", False): + return + + Pin().remove_from(rq) + + # Unpatch rq.job.Job + Pin().remove_from(rq.job.Job) + trace_utils.unwrap(rq.job.Job, "perform") + + # Unpatch rq.queue.Queue + Pin().remove_from(rq.queue.Queue) + trace_utils.unwrap(rq.queue.Queue, "enqueue_job") + trace_utils.unwrap(rq.queue.Queue, "fetch_job") + + # Unpatch rq.worker.Worker + Pin().remove_from(rq.worker.Worker) + trace_utils.unwrap(rq.worker.Worker, "perform_job") + + setattr(rq, "_datadog_patch", False) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rq/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rq/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..200459e5c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/rq/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sanic/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sanic/__init__.py new file mode 100644 index 000000000..09bd5fc57 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sanic/__init__.py @@ -0,0 +1,75 @@ +""" +The Sanic__ integration will trace requests to and from Sanic. + + +Enable Sanic tracing automatically via ``ddtrace-run``:: + + ddtrace-run python app.py + +Sanic tracing can also be enabled manually:: + + from ddtrace import patch_all + patch_all(sanic=True) + + from sanic import Sanic + from sanic.response import text + + app = Sanic(__name__) + + @app.route('/') + def index(request): + return text('hello world') + + if __name__ == '__main__': + app.run() + +If using Python 3.6, the legacy ``AsyncioContextProvider`` will have to be +enabled before using the middleware:: + + from ddtrace.contrib.asyncio.provider import AsyncioContextProvider + from ddtrace import tracer # Or whichever tracer instance you plan to use + tracer.configure(context_provider=AsyncioContextProvider()) + + +Configuration +~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.sanic['distributed_tracing_enabled'] + + Whether to parse distributed tracing headers from requests received by your Sanic app. + + Default: ``True`` + + +.. py:data:: ddtrace.config.sanic['service_name'] + + The service name reported for your Sanic app. + + Can also be configured via the ``DD_SERVICE`` environment variable. + + Default: ``'sanic'`` + + +Example:: + + from ddtrace import config + + # Enable distributed tracing + config.sanic['distributed_tracing_enabled'] = True + + # Override service name + config.sanic['service_name'] = 'custom-service-name' + +.. __: https://sanic.readthedocs.io/en/latest/ +""" +from ...utils.importlib import require_modules + + +required_modules = ["sanic"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = ["patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sanic/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sanic/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..78f79ad37 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sanic/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sanic/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sanic/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..94792aee7 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sanic/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sanic/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sanic/patch.py new file mode 100644 index 000000000..41f1bfa2b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sanic/patch.py @@ -0,0 +1,177 @@ +import asyncio + +import sanic + +import ddtrace +from ddtrace import config +from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY +from ddtrace.ext import SpanTypes +from ddtrace.pin import Pin +from ddtrace.utils.wrappers import unwrap as _u +from ddtrace.vendor import wrapt +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from .. import trace_utils +from ...internal.logger import get_logger + + +log = get_logger(__name__) + +config._add("sanic", dict(_default_service="sanic", distributed_tracing=True)) + +SANIC_PRE_21 = None + + +def update_span(span, response): + if isinstance(response, sanic.response.BaseHTTPResponse): + status_code = response.status + response_headers = response.headers + else: + # invalid response causes ServerError exception which must be handled + status_code = 500 + response_headers = None + trace_utils.set_http_meta(span, config.sanic, status_code=status_code, response_headers=response_headers) + + +def _wrap_response_callback(span, callback): + # Only for sanic 20 and older + # Wrap response callbacks (either sync or async function) to set HTTP + # response span tags + + @wrapt.function_wrapper + def wrap_sync(wrapped, instance, args, kwargs): + r = wrapped(*args, **kwargs) + response = args[0] + update_span(span, response) + return r + + @wrapt.function_wrapper + async def wrap_async(wrapped, instance, args, kwargs): + r = await wrapped(*args, **kwargs) + response = args[0] + update_span(span, response) + return r + + if asyncio.iscoroutinefunction(callback): + return wrap_async(callback) + + return wrap_sync(callback) + + +async def patch_request_respond(wrapped, instance, args, kwargs): + # Only for sanic 21 and newer + # Wrap the framework response to set HTTP response span tags + response = await wrapped(*args, **kwargs) + pin = Pin._find(instance.ctx) + if pin is not None and pin.enabled(): + span = pin.tracer.current_span() + if span is not None: + update_span(span, response) + return response + + +def _get_path(request): + """Get path and replace path parameter values with names if route exists.""" + path = request.path + try: + match_info = request.match_info + except sanic.exceptions.SanicException: + return path + for key, value in match_info.items(): + try: + value = str(value) + except Exception: + # Best effort + continue + path = path.replace(value, f"<{key}>") + return path + + +async def patch_run_request_middleware(wrapped, instance, args, kwargs): + # Set span resource from the framework request + request = args[0] + pin = Pin._find(request.ctx) + if pin is not None and pin.enabled(): + span = pin.tracer.current_span() + if span is not None: + span.resource = "{} {}".format(request.method, _get_path(request)) + return await wrapped(*args, **kwargs) + + +def patch(): + """Patch the instrumented methods.""" + global SANIC_PRE_21 + + if getattr(sanic, "__datadog_patch", False): + return + setattr(sanic, "__datadog_patch", True) + + SANIC_PRE_21 = sanic.__version__[:2] < "21" + + _w("sanic", "Sanic.handle_request", patch_handle_request) + if not SANIC_PRE_21: + _w("sanic", "Sanic._run_request_middleware", patch_run_request_middleware) + _w(sanic.request, "Request.respond", patch_request_respond) + + +def unpatch(): + """Unpatch the instrumented methods.""" + _u(sanic.Sanic, "handle_request") + if not SANIC_PRE_21: + _u(sanic.Sanic, "_run_request_middleware") + _u(sanic.request.Request, "respond") + if not getattr(sanic, "__datadog_patch", False): + return + setattr(sanic, "__datadog_patch", False) + + +async def patch_handle_request(wrapped, instance, args, kwargs): + """Wrapper for Sanic.handle_request""" + + def unwrap(request, write_callback=None, stream_callback=None, **kwargs): + return request, write_callback, stream_callback, kwargs + + request, write_callback, stream_callback, new_kwargs = unwrap(*args, **kwargs) + + if request.scheme not in ("http", "https"): + return await wrapped(*args, **kwargs) + + pin = Pin() + if SANIC_PRE_21: + # Set span resource from the framework request + resource = "{} {}".format(request.method, _get_path(request)) + else: + # The path is not available anymore in 21.x. Get it from + # the _run_request_middleware instrumented method. + resource = None + pin.onto(request.ctx) + + headers = request.headers.copy() + + trace_utils.activate_distributed_headers(ddtrace.tracer, int_config=config.sanic, request_headers=headers) + + with pin.tracer.trace( + "sanic.request", + service=trace_utils.int_service(None, config.sanic), + resource=resource, + span_type=SpanTypes.WEB, + ) as span: + sample_rate = config.sanic.get_analytics_sample_rate(use_global_config=True) + if sample_rate is not None: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate) + + method = request.method + url = "{scheme}://{host}{path}".format(scheme=request.scheme, host=request.host, path=request.path) + query_string = request.query_string + if isinstance(query_string, bytes): + query_string = query_string.decode() + trace_utils.set_http_meta( + span, config.sanic, method=method, url=url, query=query_string, request_headers=headers + ) + + if write_callback is not None: + new_kwargs["write_callback"] = _wrap_response_callback(span, write_callback) + if stream_callback is not None: + new_kwargs["stream_callback"] = _wrap_response_callback(span, stream_callback) + + return await wrapped(request, **new_kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/snowflake/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/snowflake/__init__.py new file mode 100644 index 000000000..a178e7f9a --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/snowflake/__init__.py @@ -0,0 +1,71 @@ +""" +The snowflake integration instruments the ``snowflake-connector-python`` library to trace Snowflake queries. + +Note that this integration is in beta. + +Enabling +~~~~~~~~ + +The integration is not enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch, patch_all + patch(snowflake=True) + patch_all(snowflake=True) + +or the ``DD_TRACE_SNOWFLAKE_ENABLED=true`` to enable it with ``ddtrace-run``. + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.snowflake["service"] + + The service name reported by default for snowflake spans. + + This option can also be set with the ``DD_SNOWFLAKE_SERVICE`` environment + variable. + + Default: ``"snowflake"`` + +.. py:data:: ddtrace.config.snowflake["trace_fetch_methods"] + + Whether or not to trace fetch methods. + + Can also configured via the ``DD_SNOWFLAKE_TRACE_FETCH_METHODS`` environment variable. + + Default: ``False`` + + +Instance Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +To configure the integration on an per-connection basis use the +``Pin`` API:: + + from ddtrace import Pin + from snowflake.connector import connect + + # This will report a span with the default settings + conn = connect(user="alice", password="b0b", account="dev") + + # Use a pin to override the service name for this connection. + Pin.override(conn, service="snowflake-dev") + + + cursor = conn.cursor() + cursor.execute("SELECT current_version()") +""" +from ...utils.importlib import require_modules + + +required_modules = ["snowflake.connector"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = ["patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/snowflake/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/snowflake/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..a13c8fb05 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/snowflake/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/snowflake/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/snowflake/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..e63c9d06a Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/snowflake/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/snowflake/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/snowflake/patch.py new file mode 100644 index 000000000..b782fa6e7 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/snowflake/patch.py @@ -0,0 +1,62 @@ +from ddtrace import Pin +from ddtrace import config +from ddtrace.vendor import wrapt + +from ...ext import db +from ...ext import net +from ...utils.formats import asbool +from ...utils.formats import get_env +from ...utils.wrappers import unwrap +from ..dbapi import TracedConnection + + +config._add( + "snowflake", + dict( + _default_service="snowflake", + trace_fetch_methods=asbool(get_env("snowflake", "trace_fetch_methods", default=False)), + ), +) + + +def patch(): + import snowflake.connector + + if getattr(snowflake.connector, "_datadog_patch", False): + return + setattr(snowflake.connector, "_datadog_patch", True) + + wrapt.wrap_function_wrapper(snowflake.connector, "Connect", patched_connect) + wrapt.wrap_function_wrapper(snowflake.connector, "connect", patched_connect) + + +def unpatch(): + import snowflake.connector + + if getattr(snowflake.connector, "_datadog_patch", False): + setattr(snowflake.connector, "_datadog_patch", False) + + unwrap(snowflake.connector, "Connect") + unwrap(snowflake.connector, "connect") + + +def patched_connect(connect_func, _, args, kwargs): + conn = connect_func(*args, **kwargs) + if isinstance(conn, TracedConnection): + return conn + + # Add default tags to each query + tags = { + net.TARGET_HOST: conn.host, + net.TARGET_PORT: conn.port, + db.NAME: conn.database, + db.USER: conn.user, + "db.application": conn.application, + "db.schema": conn.schema, + "db.warehouse": conn.warehouse, + } + + pin = Pin(tags=tags) + traced_conn = TracedConnection(conn, pin=pin, cfg=config.snowflake) + pin.onto(traced_conn) + return traced_conn diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/__init__.py new file mode 100644 index 000000000..69b414a75 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/__init__.py @@ -0,0 +1,29 @@ +""" +To trace sqlalchemy queries, add instrumentation to the engine class +using the patch method that **must be called before** importing sqlalchemy:: + + # patch before importing `create_engine` + from ddtrace import Pin, patch + patch(sqlalchemy=True) + + # use SQLAlchemy as usual + from sqlalchemy import create_engine + + engine = create_engine('sqlite:///:memory:') + engine.connect().execute("SELECT COUNT(*) FROM users") + + # Use a PIN to specify metadata related to this engine + Pin.override(engine, service='replica-db') +""" +from ...utils.importlib import require_modules + + +required_modules = ["sqlalchemy", "sqlalchemy.event"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .engine import trace_engine + from .patch import patch + from .patch import unpatch + + __all__ = ["trace_engine", "patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..457eaf154 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/__pycache__/engine.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/__pycache__/engine.cpython-38.pyc new file mode 100644 index 000000000..29607b4ee Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/__pycache__/engine.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..463ffafc8 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/engine.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/engine.py new file mode 100644 index 000000000..8662864a7 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/engine.py @@ -0,0 +1,153 @@ +""" +To trace sqlalchemy queries, add instrumentation to the engine class or +instance you are using:: + + from ddtrace import tracer + from ddtrace.contrib.sqlalchemy import trace_engine + from sqlalchemy import create_engine + + engine = create_engine('sqlite:///:memory:') + trace_engine(engine, tracer, 'my-database') + + engine.connect().execute('select count(*) from users') +""" +# 3p +import sqlalchemy +from sqlalchemy.event import listen + +# project +import ddtrace +from ddtrace import config + +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import net as netx +from ...ext import sql as sqlx +from ...pin import Pin + + +def trace_engine(engine, tracer=None, service=None): + """ + Add tracing instrumentation to the given sqlalchemy engine or instance. + + :param sqlalchemy.Engine engine: a SQLAlchemy engine class or instance + :param ddtrace.Tracer tracer: a tracer instance. will default to the global + :param str service: the name of the service to trace. + """ + tracer = tracer or ddtrace.tracer # by default use global + EngineTracer(tracer, service, engine) + + +def _wrap_create_engine(func, module, args, kwargs): + """Trace the SQLAlchemy engine, creating an `EngineTracer` + object that will listen to SQLAlchemy events. A PIN object + is attached to the engine instance so that it can be + used later. + """ + # the service name is set to `None` so that the engine + # name is used by default; users can update this setting + # using the PIN object + engine = func(*args, **kwargs) + EngineTracer(ddtrace.tracer, None, engine) + return engine + + +class EngineTracer(object): + def __init__(self, tracer, service, engine): + self.tracer = tracer + self.engine = engine + self.vendor = sqlx.normalize_vendor(engine.name) + self.service = service or self.vendor + self.name = "%s.query" % self.vendor + + # attach the PIN + Pin(app=self.vendor, tracer=tracer, service=self.service).onto(engine) + + listen(engine, "before_cursor_execute", self._before_cur_exec) + listen(engine, "after_cursor_execute", self._after_cur_exec) + + # Determine name of error event to listen for + # Ref: https://github.com/DataDog/dd-trace-py/issues/841 + if sqlalchemy.__version__[0] != "0": + error_event = "handle_error" + else: + error_event = "dbapi_error" + listen(engine, error_event, self._handle_db_error) + + def _before_cur_exec(self, conn, cursor, statement, *args): + pin = Pin.get_from(self.engine) + if not pin or not pin.enabled(): + # don't trace the execution + return + + span = pin.tracer.trace( + self.name, + service=pin.service, + span_type=SpanTypes.SQL, + resource=statement, + ) + span.set_tag(SPAN_MEASURED_KEY) + + if not _set_tags_from_url(span, conn.engine.url): + _set_tags_from_cursor(span, self.vendor, cursor) + + # set analytics sample rate + sample_rate = config.sqlalchemy.get_analytics_sample_rate() + if sample_rate is not None: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate) + + def _after_cur_exec(self, conn, cursor, statement, *args): + pin = Pin.get_from(self.engine) + if not pin or not pin.enabled(): + # don't trace the execution + return + + span = pin.tracer.current_span() + if not span: + return + + try: + if cursor and cursor.rowcount >= 0: + span.set_tag(sqlx.ROWS, cursor.rowcount) + finally: + span.finish() + + def _handle_db_error(self, *args): + pin = Pin.get_from(self.engine) + if not pin or not pin.enabled(): + # don't trace the execution + return + + span = pin.tracer.current_span() + if not span: + return + + try: + span.set_traceback() + finally: + span.finish() + + +def _set_tags_from_url(span, url): + """set connection tags from the url. return true if successful.""" + if url.host: + span.set_tag(netx.TARGET_HOST, url.host) + if url.port: + span.set_tag(netx.TARGET_PORT, url.port) + if url.database: + span.set_tag(sqlx.DB, url.database) + + return bool(span.get_tag(netx.TARGET_HOST)) + + +def _set_tags_from_cursor(span, vendor, cursor): + """attempt to set db connection tags by introspecting the cursor.""" + if "postgres" == vendor: + if hasattr(cursor, "connection"): + dsn = getattr(cursor.connection, "dsn", None) + if dsn: + d = sqlx.parse_pg_dsn(dsn) + span._set_str_tag(sqlx.DB, d.get("dbname")) + span._set_str_tag(netx.TARGET_HOST, d.get("host")) + span.set_metric(netx.TARGET_PORT, int(d.get("port"))) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/patch.py new file mode 100644 index 000000000..9c364045a --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlalchemy/patch.py @@ -0,0 +1,24 @@ +import sqlalchemy + +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from ...utils.wrappers import unwrap +from .engine import _wrap_create_engine + + +def patch(): + if getattr(sqlalchemy.engine, "__datadog_patch", False): + return + setattr(sqlalchemy.engine, "__datadog_patch", True) + + # patch the engine creation function + _w("sqlalchemy", "create_engine", _wrap_create_engine) + _w("sqlalchemy.engine", "create_engine", _wrap_create_engine) + + +def unpatch(): + # unpatch sqlalchemy + if getattr(sqlalchemy.engine, "__datadog_patch", False): + setattr(sqlalchemy.engine, "__datadog_patch", False) + unwrap(sqlalchemy, "create_engine") + unwrap(sqlalchemy.engine, "create_engine") diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/__init__.py new file mode 100644 index 000000000..da4f1c04d --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/__init__.py @@ -0,0 +1,60 @@ +""" +The sqlite integration instruments the built-in sqlite module to trace SQLite queries. + + +Enabling +~~~~~~~~ + +The integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + patch(sqlite=True) + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.sqlite["service"] + + The service name reported by default for sqlite spans. + + This option can also be set with the ``DD_SQLITE_SERVICE`` environment + variable. + + Default: ``"sqlite"`` + +.. py:data:: ddtrace.config.sqlite["trace_fetch_methods"] + + Whether or not to trace fetch methods. + + Can also configured via the ``DD_SQLITE_TRACE_FETCH_METHODS`` environment variable. + + Default: ``False`` + + +Instance Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +To configure the integration on an per-connection basis use the +``Pin`` API:: + + from ddtrace import Pin + import sqlite3 + + # This will report a span with the default settings + db = sqlite3.connect(":memory:") + + # Use a pin to override the service name for the connection. + Pin.override(db, service='sqlite-users') + + cursor = db.cursor() + cursor.execute("select * from users where id = 1") +""" +from .connection import connection_factory +from .patch import patch + + +__all__ = ["connection_factory", "patch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..add9318bb Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/__pycache__/connection.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/__pycache__/connection.cpython-38.pyc new file mode 100644 index 000000000..058ce8b16 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/__pycache__/connection.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..e098e911e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/connection.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/connection.py new file mode 100644 index 000000000..3efa5577c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/connection.py @@ -0,0 +1,8 @@ +from sqlite3 import Connection + +from ...utils.deprecation import deprecated + + +@deprecated(message="Use patching instead (see the docs).", version="1.0.0") +def connection_factory(*args, **kwargs): + return Connection diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/patch.py new file mode 100644 index 000000000..41ed07c01 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/sqlite3/patch.py @@ -0,0 +1,78 @@ +# 3p +import sqlite3 +import sqlite3.dbapi2 + +from ddtrace import config +from ddtrace.vendor import wrapt + +# project +from ...contrib.dbapi import FetchTracedCursor +from ...contrib.dbapi import TracedConnection +from ...contrib.dbapi import TracedCursor +from ...pin import Pin +from ...utils.formats import asbool +from ...utils.formats import get_env + + +# Original connect method +_connect = sqlite3.connect + +config._add( + "sqlite", + dict( + _default_service="sqlite", + trace_fetch_methods=asbool(get_env("sqlite", "trace_fetch_methods", default=False)), + ), +) + + +def patch(): + wrapped = wrapt.FunctionWrapper(_connect, traced_connect) + + setattr(sqlite3, "connect", wrapped) + setattr(sqlite3.dbapi2, "connect", wrapped) + + +def unpatch(): + sqlite3.connect = _connect + sqlite3.dbapi2.connect = _connect + + +def traced_connect(func, _, args, kwargs): + conn = func(*args, **kwargs) + return patch_conn(conn) + + +def patch_conn(conn): + wrapped = TracedSQLite(conn) + Pin(app="sqlite").onto(wrapped) + return wrapped + + +class TracedSQLiteCursor(TracedCursor): + def executemany(self, *args, **kwargs): + # DEV: SQLite3 Cursor.execute always returns back the cursor instance + super(TracedSQLiteCursor, self).executemany(*args, **kwargs) + return self + + def execute(self, *args, **kwargs): + # DEV: SQLite3 Cursor.execute always returns back the cursor instance + super(TracedSQLiteCursor, self).execute(*args, **kwargs) + return self + + +class TracedSQLiteFetchCursor(TracedSQLiteCursor, FetchTracedCursor): + pass + + +class TracedSQLite(TracedConnection): + def __init__(self, conn, pin=None, cursor_cls=None): + if not cursor_cls: + # Do not trace `fetch*` methods by default + cursor_cls = TracedSQLiteFetchCursor if config.sqlite.trace_fetch_methods else TracedSQLiteCursor + + super(TracedSQLite, self).__init__(conn, pin=pin, cfg=config.sqlite, cursor_cls=cursor_cls) + + def execute(self, *args, **kwargs): + # sqlite has a few extra sugar functions + return self.cursor().execute(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/starlette/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/starlette/__init__.py new file mode 100644 index 000000000..d08bab632 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/starlette/__init__.py @@ -0,0 +1,86 @@ +""" +The Starlette integration will trace requests to and from Starlette. + + +Enabling +~~~~~~~~ + +The starlette integration is enabled automatically when using +:ref:`ddtrace-run` or :ref:`patch_all()`. + +Or use :ref:`patch()` to manually enable the integration:: + + from ddtrace import patch + from starlette.applications import Starlette + + patch(starlette=True) + app = Starlette() + + +If using Python 3.6, the legacy ``AsyncioContextProvider`` will have to be +enabled before using the middleware:: + + from ddtrace.contrib.asyncio.provider import AsyncioContextProvider + from ddtrace import tracer # Or whichever tracer instance you plan to use + tracer.configure(context_provider=AsyncioContextProvider()) + + +Configuration +~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.starlette['distributed_tracing'] + + Whether to parse distributed tracing headers from requests received by your Starlette app. + + Can also be enabled with the ``DD_STARLETTE_DISTRIBUTED_TRACING`` environment variable. + + Default: ``True`` + +.. py:data:: ddtrace.config.starlette['analytics_enabled'] + + Whether to analyze spans for starlette in App Analytics. + + Can also be enabled with the ``DD_STARLETTE_ANALYTICS_ENABLED`` environment variable. + + Default: ``None`` + +.. py:data:: ddtrace.config.starlette['service_name'] + + The service name reported for your starlette app. + + Can also be configured via the ``DD_SERVICE`` environment variable. + + Default: ``'starlette'`` + +.. py:data:: ddtrace.config.starlette['request_span_name'] + + The span name for a starlette request. + + Default: ``'starlette.request'`` + + +Example:: + + from ddtrace import config + + # Enable distributed tracing + config.starlette['distributed_tracing'] = True + + # Override service name + config.starlette['service_name'] = 'custom-service-name' + + # Override request span name + config.starlette['request_span_name'] = 'custom-request-span-name' + +""" +from ...utils.importlib import require_modules + + +required_modules = ["starlette"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = ["patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/starlette/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/starlette/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..5bd56e72b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/starlette/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/starlette/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/starlette/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..949a3e5c6 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/starlette/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/starlette/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/starlette/patch.py new file mode 100644 index 000000000..8c9108156 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/starlette/patch.py @@ -0,0 +1,67 @@ +import starlette +from starlette.middleware import Middleware +from starlette.routing import Match + +from ddtrace import config +from ddtrace.contrib.asgi.middleware import TraceMiddleware +from ddtrace.internal.logger import get_logger +from ddtrace.utils.wrappers import unwrap as _u +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + + +log = get_logger(__name__) + +config._add( + "starlette", + dict( + _default_service="starlette", + request_span_name="starlette.request", + distributed_tracing=True, + aggregate_resources=True, + ), +) + + +def get_resource(scope): + path = None + routes = scope["app"].routes + for route in routes: + match, _ = route.matches(scope) + if match == Match.FULL: + path = route.path + break + elif match == Match.PARTIAL and path is None: + path = route.path + return path + + +def span_modifier(span, scope): + resource = get_resource(scope) + if config.starlette["aggregate_resources"] and resource: + span.resource = "{} {}".format(scope["method"], resource) + + +def traced_init(wrapped, instance, args, kwargs): + mw = kwargs.pop("middleware", []) + mw.insert(0, Middleware(TraceMiddleware, integration_config=config.starlette, span_modifier=span_modifier)) + kwargs.update({"middleware": mw}) + + wrapped(*args, **kwargs) + + +def patch(): + if getattr(starlette, "_datadog_patch", False): + return + + setattr(starlette, "_datadog_patch", True) + + _w("starlette.applications", "Starlette.__init__", traced_init) + + +def unpatch(): + if not getattr(starlette, "_datadog_patch", False): + return + + setattr(starlette, "_datadog_patch", False) + + _u(starlette.applications.Starlette, "__init__") diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__init__.py new file mode 100644 index 000000000..bbf7216fd --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__init__.py @@ -0,0 +1,128 @@ +r""" +The Tornado integration traces all ``RequestHandler`` defined in a Tornado web application. +Auto instrumentation is available using the ``patch`` function that **must be called before** +importing the tornado library. + +**Note:** This integration requires Python 3.7 and above for Tornado 5 and 6. + +The following is an example:: + + # patch before importing tornado and concurrent.futures + from ddtrace import tracer, patch + patch(tornado=True) + + import tornado.web + import tornado.gen + import tornado.ioloop + + # create your handlers + class MainHandler(tornado.web.RequestHandler): + @tornado.gen.coroutine + def get(self): + self.write("Hello, world") + + # create your application + app = tornado.web.Application([ + (r'/', MainHandler), + ]) + + # and run it as usual + app.listen(8888) + tornado.ioloop.IOLoop.current().start() + +When any type of ``RequestHandler`` is hit, a request root span is automatically created. If +you want to trace more parts of your application, you can use the ``wrap()`` decorator and +the ``trace()`` method as usual:: + + class MainHandler(tornado.web.RequestHandler): + @tornado.gen.coroutine + def get(self): + yield self.notify() + yield self.blocking_method() + with tracer.trace('tornado.before_write') as span: + # trace more work in the handler + + @tracer.wrap('tornado.executor_handler') + @tornado.concurrent.run_on_executor + def blocking_method(self): + # do something expensive + + @tracer.wrap('tornado.notify', service='tornado-notification') + @tornado.gen.coroutine + def notify(self): + # do something + +If you are overriding the ``on_finish`` or ``log_exception`` methods on a +``RequestHandler``, you will need to call the super method to ensure the +tracer's patched methods are called:: + + class MainHandler(tornado.web.RequestHandler): + @tornado.gen.coroutine + def get(self): + self.write("Hello, world") + + def on_finish(self): + super(MainHandler, self).on_finish() + # do other clean-up + + def log_exception(self, typ, value, tb): + super(MainHandler, self).log_exception(typ, value, tb) + # do other logging + +Tornado settings can be used to change some tracing configuration, like:: + + settings = { + 'datadog_trace': { + 'default_service': 'my-tornado-app', + 'tags': {'env': 'production'}, + 'distributed_tracing': False, + 'settings': { + 'FILTERS': [ + FilterRequestsOnUrl(r'http://test\\.example\\.com'), + ], + }, + }, + } + + app = tornado.web.Application([ + (r'/', MainHandler), + ], **settings) + +The available settings are: + +* ``default_service`` (default: `tornado-web`): set the service name used by the tracer. Usually + this configuration must be updated with a meaningful name. Can also be configured via the + ``DD_SERVICE`` environment variable. +* ``tags`` (default: `{}`): set global tags that should be applied to all spans. +* ``enabled`` (default: `True`): define if the tracer is enabled or not. If set to `false`, the + code is still instrumented but no spans are sent to the APM agent. +* ``distributed_tracing`` (default: `None`): enable distributed tracing if this is called + remotely from an instrumented application. Overrides the integration config which is configured via the + ``DD_TORNADO_DISTRIBUTED_TRACING`` environment variable. + We suggest to enable it only for internal services where headers are under your control. +* ``agent_hostname`` (default: `localhost`): define the hostname of the APM agent. +* ``agent_port`` (default: `8126`): define the port of the APM agent. +* ``settings`` (default: ``{}``): Tracer extra settings used to change, for instance, the filtering behavior. +""" +from ...utils.importlib import require_modules + + +required_modules = ["tornado"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .stack_context import TracerStackContext + from .stack_context import run_with_trace_context + + context_provider = TracerStackContext() + + from .patch import patch + from .patch import unpatch + + __all__ = [ + "patch", + "unpatch", + "context_provider", + "run_with_trace_context", + "TracerStackContext", + ] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..c41c6e013 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/application.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/application.cpython-38.pyc new file mode 100644 index 000000000..3408abcbc Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/application.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/compat.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/compat.cpython-38.pyc new file mode 100644 index 000000000..55555bb3b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/compat.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/constants.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/constants.cpython-38.pyc new file mode 100644 index 000000000..2ecaa5c00 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/constants.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/decorators.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/decorators.cpython-38.pyc new file mode 100644 index 000000000..859dc4f27 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/decorators.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/handlers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/handlers.cpython-38.pyc new file mode 100644 index 000000000..7d5297990 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/handlers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..649bc6c7d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/stack_context.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/stack_context.cpython-38.pyc new file mode 100644 index 000000000..f137b9917 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/stack_context.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/template.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/template.cpython-38.pyc new file mode 100644 index 000000000..b175f8959 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/__pycache__/template.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/application.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/application.py new file mode 100644 index 000000000..d6cee3286 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/application.py @@ -0,0 +1,58 @@ +from tornado import template + +import ddtrace +from ddtrace import config + +from . import context_provider +from . import decorators +from .constants import CONFIG_KEY + + +def tracer_config(__init__, app, args, kwargs): + """ + Wrap Tornado web application so that we can configure services info and + tracing settings after the initialization. + """ + # call the Application constructor + __init__(*args, **kwargs) + + # default settings + settings = { + "tracer": ddtrace.tracer, + "default_service": config._get_service("tornado-web"), + "distributed_tracing": None, + "analytics_enabled": None, + } + + # update defaults with users settings + user_settings = app.settings.get(CONFIG_KEY) + if user_settings: + settings.update(user_settings) + + app.settings[CONFIG_KEY] = settings + tracer = settings["tracer"] + service = settings["default_service"] + + # extract extra settings + extra_settings = settings.get("settings", {}) + + # the tracer must use the right Context propagation and wrap executor; + # this action is done twice because the patch() method uses the + # global tracer while here we can have a different instance (even if + # this is not usual). + tracer.configure( + context_provider=context_provider, + wrap_executor=decorators.wrap_executor, + enabled=settings.get("enabled", None), + hostname=settings.get("agent_hostname", None), + port=settings.get("agent_port", None), + settings=extra_settings, + ) + + # set global tags if any + tags = settings.get("tags", None) + if tags: + tracer.set_tags(tags) + + # configure the PIN object for template rendering + ddtrace.Pin(app="tornado", service=service, tracer=tracer).onto(template) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/compat.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/compat.py new file mode 100644 index 000000000..0c282d784 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/compat.py @@ -0,0 +1,16 @@ +try: + # detect if concurrent.futures is available as a Python + # stdlib or Python 2.7 backport + from ..futures import patch as wrap_futures + from ..futures import unpatch as unwrap_futures + + futures_available = True +except ImportError: + + def wrap_futures(): + pass + + def unwrap_futures(): + pass + + futures_available = False diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/constants.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/constants.py new file mode 100644 index 000000000..18a3a6eb5 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/constants.py @@ -0,0 +1,7 @@ +""" +This module defines Tornado settings that are shared between +integration modules. +""" +CONFIG_KEY = "datadog_trace" +REQUEST_SPAN_KEY = "__datadog_request_span" +FUTURE_SPAN_KEY = "__datadog_future_span" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/decorators.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/decorators.py new file mode 100644 index 000000000..6247a52e4 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/decorators.py @@ -0,0 +1,77 @@ +import sys + +from .constants import FUTURE_SPAN_KEY + + +def _finish_span(future): + """ + Finish the span if it's attached to the given ``Future`` object. + This method is a Tornado callback used to close a decorated function + executed as a coroutine or as a synchronous function in another thread. + """ + span = getattr(future, FUTURE_SPAN_KEY, None) + + if span: + # `tornado.concurrent.Future` in PY3 tornado>=4.0,<5 has `exc_info` + if callable(getattr(future, "exc_info", None)): + # retrieve the exception from the coroutine object + exc_info = future.exc_info() + if exc_info: + span.set_exc_info(*exc_info) + elif callable(getattr(future, "exception", None)): + # in tornado>=4.0,<5 with PY2 `concurrent.futures._base.Future` + # `exception_info()` returns `(exception, traceback)` but + # `exception()` only returns the first element in the tuple + if callable(getattr(future, "exception_info", None)): + exc, exc_tb = future.exception_info() + if exc and exc_tb: + exc_type = type(exc) + span.set_exc_info(exc_type, exc, exc_tb) + # in tornado>=5 with PY3, `tornado.concurrent.Future` is alias to + # `asyncio.Future` in PY3 `exc_info` not available, instead use + # exception method + else: + exc = future.exception() + if exc: + # we expect exception object to have a traceback attached + if hasattr(exc, "__traceback__"): + exc_type = type(exc) + exc_tb = getattr(exc, "__traceback__", None) + span.set_exc_info(exc_type, exc, exc_tb) + # if all else fails use currently handled exception for + # current thread + else: + span.set_exc_info(*sys.exc_info()) + + span.finish() + + +def wrap_executor(tracer, fn, args, kwargs, span_name, service=None, resource=None, span_type=None): + """ + Wrap executor function used to change the default behavior of + ``Tracer.wrap()`` method. A decorated Tornado function can be + a regular function or a coroutine; if a coroutine is decorated, a + span is attached to the returned ``Future`` and a callback is set + so that it will close the span when the ``Future`` is done. + """ + span = tracer.trace(span_name, service=service, resource=resource, span_type=span_type) + + # catch standard exceptions raised in synchronous executions + try: + future = fn(*args, **kwargs) + + # duck-typing: if it has `add_done_callback` it's a Future + # object whatever is the underlying implementation + if callable(getattr(future, "add_done_callback", None)): + setattr(future, FUTURE_SPAN_KEY, span) + future.add_done_callback(_finish_span) + else: + # we don't have a future so the `future` variable + # holds the result of the function + span.finish() + except Exception: + span.set_traceback() + span.finish() + raise + + return future diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/handlers.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/handlers.py new file mode 100644 index 000000000..a5d6cb333 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/handlers.py @@ -0,0 +1,113 @@ +from tornado.web import HTTPError + +from ddtrace import config + +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...utils import ArgumentError +from ...utils import get_argument_value +from ..trace_utils import set_http_meta +from .constants import CONFIG_KEY +from .constants import REQUEST_SPAN_KEY +from .stack_context import TracerStackContext + + +def execute(func, handler, args, kwargs): + """ + Wrap the handler execute method so that the entire request is within the same + ``TracerStackContext``. This simplifies users code when the automatic ``Context`` + retrieval is used via ``Tracer.trace()`` method. + """ + # retrieve tracing settings + settings = handler.settings[CONFIG_KEY] + tracer = settings["tracer"] + service = settings["default_service"] + distributed_tracing = settings["distributed_tracing"] + + with TracerStackContext(): + trace_utils.activate_distributed_headers( + tracer, int_config=config.tornado, request_headers=handler.request.headers, override=distributed_tracing + ) + + # store the request span in the request so that it can be used later + request_span = tracer.trace( + "tornado.request", + service=service, + span_type=SpanTypes.WEB, + ) + request_span.set_tag(SPAN_MEASURED_KEY) + # set analytics sample rate + # DEV: tornado is special case maintains separate configuration from config api + analytics_enabled = settings["analytics_enabled"] + if (config.analytics_enabled and analytics_enabled is not False) or analytics_enabled is True: + request_span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, settings.get("analytics_sample_rate", True)) + + setattr(handler.request, REQUEST_SPAN_KEY, request_span) + + return func(*args, **kwargs) + + +def on_finish(func, handler, args, kwargs): + """ + Wrap the ``RequestHandler.on_finish`` method. This is the last executed method + after the response has been sent, and it's used to retrieve and close the + current request span (if available). + """ + request = handler.request + request_span = getattr(request, REQUEST_SPAN_KEY, None) + if request_span: + # use the class name as a resource; if an handler is not available, the + # default handler class will be used so we don't pollute the resource + # space here + klass = handler.__class__ + request_span.resource = "{}.{}".format(klass.__module__, klass.__name__) + set_http_meta( + request_span, + config.tornado, + method=request.method, + url=request.full_url().rsplit("?", 1)[0], + status_code=handler.get_status(), + query=request.query, + ) + request_span.finish() + + return func(*args, **kwargs) + + +def log_exception(func, handler, args, kwargs): + """ + Wrap the ``RequestHandler.log_exception``. This method is called when an + Exception is not handled in the user code. In this case, we save the exception + in the current active span. If the Tornado ``Finish`` exception is raised, this wrapper + will not be called because ``Finish`` is not an exception. + """ + # safe-guard: expected arguments -> log_exception(self, typ, value, tb) + try: + value = get_argument_value(args, kwargs, 1, "value") + except ArgumentError: + value = None + + if not value: + return func(*args, **kwargs) + + # retrieve the current span + tracer = handler.settings[CONFIG_KEY]["tracer"] + current_span = tracer.current_span() + + if not current_span: + return func(*args, **kwargs) + + if isinstance(value, HTTPError): + # Tornado uses HTTPError exceptions to stop and return a status code that + # is not a 2xx. In this case we want to check the status code to be sure that + # only 5xx are traced as errors, while any other HTTPError exception is handled as + # usual. + if 500 <= value.status_code <= 599: + current_span.set_exc_info(*args) + else: + # any other uncaught exception should be reported as error + current_span.set_exc_info(*args) + + return func(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/patch.py new file mode 100644 index 000000000..d6cc86f3e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/patch.py @@ -0,0 +1,73 @@ +import tornado + +import ddtrace +from ddtrace import config +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from . import application +from . import compat +from . import context_provider +from . import decorators +from . import handlers +from . import template +from ...utils.formats import asbool +from ...utils.formats import get_env +from ...utils.wrappers import unwrap as _u + + +config._add( + "tornado", + dict( + distributed_tracing=asbool(get_env("tornado", "distributed_tracing", default=True)), + ), +) + + +def patch(): + """ + Tracing function that patches the Tornado web application so that it will be + traced using the given ``tracer``. + """ + # patch only once + if getattr(tornado, "__datadog_patch", False): + return + setattr(tornado, "__datadog_patch", True) + + # patch Application to initialize properly our settings and tracer + _w("tornado.web", "Application.__init__", application.tracer_config) + + # patch RequestHandler to trace all Tornado handlers + _w("tornado.web", "RequestHandler._execute", handlers.execute) + _w("tornado.web", "RequestHandler.on_finish", handlers.on_finish) + _w("tornado.web", "RequestHandler.log_exception", handlers.log_exception) + + # patch Template system + _w("tornado.template", "Template.generate", template.generate) + + # patch Python Futures if available when an Executor pool is used + compat.wrap_futures() + + # configure the global tracer + ddtrace.tracer.configure( + context_provider=context_provider, + wrap_executor=decorators.wrap_executor, + ) + + +def unpatch(): + """ + Remove all tracing functions in a Tornado web application. + """ + if not getattr(tornado, "__datadog_patch", False): + return + setattr(tornado, "__datadog_patch", False) + + # unpatch Tornado + _u(tornado.web.RequestHandler, "_execute") + _u(tornado.web.RequestHandler, "on_finish") + _u(tornado.web.RequestHandler, "log_exception") + _u(tornado.web.Application, "__init__") + _u(tornado.template.Template, "generate") + + # unpatch `futures` + compat.unwrap_futures() diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/stack_context.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/stack_context.py new file mode 100644 index 000000000..e116b0e52 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/stack_context.py @@ -0,0 +1,147 @@ +import sys + +import tornado +from tornado.ioloop import IOLoop + +from ...provider import BaseContextProvider +from ...provider import DefaultContextProvider +from ...span import Span + + +# tornado.stack_context deprecated in Tornado 5 removed in Tornado 6 +# instead use DefaultContextProvider with ContextVarContextManager for asyncio +_USE_STACK_CONTEXT = not (sys.version_info >= (3, 7) and tornado.version_info >= (5, 0)) + +if _USE_STACK_CONTEXT: + from tornado.stack_context import StackContextInconsistentError + from tornado.stack_context import _state + + class TracerStackContext(DefaultContextProvider): + """ + A context manager that manages ``Context`` instances in a thread-local state. + It must be used every time a Tornado's handler or coroutine is used within a + tracing Context. It is meant to work like a traditional ``StackContext``, + preserving the state across asynchronous calls. + + Every time a new manager is initialized, a new ``Context()`` is created for + this execution flow. A context created in a ``TracerStackContext`` is not + shared between different threads. + + This implementation follows some suggestions provided here: + https://github.com/tornadoweb/tornado/issues/1063 + """ + + def __init__(self): + # type: (...) -> None + # HACK(jd): this should be using super(), but calling DefaultContextProvider.__init__ + # sets the context to `None` which breaks this code. + # We therefore skip DefaultContextProvider.__init__ and call only BaseContextProvider.__init__. + BaseContextProvider.__init__(self) + self._context = None + + def enter(self): + """ + Required to preserve the ``StackContext`` protocol. + """ + pass + + def exit(self, type, value, traceback): # noqa: A002 + """ + Required to preserve the ``StackContext`` protocol. + """ + pass + + def __enter__(self): + self.old_contexts = _state.contexts + self.new_contexts = (self.old_contexts[0] + (self,), self) + _state.contexts = self.new_contexts + return self + + def __exit__(self, type, value, traceback): # noqa: A002 + final_contexts = _state.contexts + _state.contexts = self.old_contexts + + if final_contexts is not self.new_contexts: + raise StackContextInconsistentError( + "stack_context inconsistency (may be caused by yield " 'within a "with TracerStackContext" block)' + ) + + # break the reference to allow faster GC on CPython + self.new_contexts = None + + def _has_io_loop(self): + """Helper to determine if we are currently in an IO loop""" + return getattr(IOLoop._current, "instance", None) is not None + + def _has_active_context(self): + """Helper to determine if we have an active context or not""" + if not self._has_io_loop(): + return super(TracerStackContext, self)._has_active_context() + else: + # we're inside a Tornado loop so the TracerStackContext is used + return self._get_state_active_context() is not None + + def _get_state_active_context(self): + """Helper to get the currently active context from the TracerStackContext""" + # we're inside a Tornado loop so the TracerStackContext is used + for stack in reversed(_state.contexts[0]): + if isinstance(stack, self.__class__): + ctx = stack._context + if isinstance(ctx, Span): + return self._update_active(ctx) + return ctx + return None + + def active(self): + """ + Return the ``Context`` from the current execution flow. This method can be + used inside a Tornado coroutine to retrieve and use the current tracing context. + If used in a separated Thread, the `_state` thread-local storage is used to + propagate the current Active context from the `MainThread`. + """ + if not self._has_io_loop(): + # if a Tornado loop is not available, it means that this method + # has been called from a synchronous code, so we can rely in a + # thread-local storage + return super(TracerStackContext, self).active() + else: + # we're inside a Tornado loop so the TracerStackContext is used + return self._get_state_active_context() + + def activate(self, ctx): + """ + Set the active ``Context`` for this async execution. If a ``TracerStackContext`` + is not found, the context is discarded. + If used in a separated Thread, the `_state` thread-local storage is used to + propagate the current Active context from the `MainThread`. + """ + if not self._has_io_loop(): + # because we're outside of an asynchronous execution, we store + # the current context in a thread-local storage + super(TracerStackContext, self).activate(ctx) + else: + # we're inside a Tornado loop so the TracerStackContext is used + for stack_ctx in reversed(_state.contexts[0]): + if isinstance(stack_ctx, self.__class__): + stack_ctx._context = ctx + return ctx + + +else: + # no-op when not using stack_context + class TracerStackContext(DefaultContextProvider): + def __enter__(self): + pass + + def __exit__(self, *exc): + pass + + +def run_with_trace_context(func, *args, **kwargs): + """ + Run the given function within a traced StackContext. This function is used to + trace Tornado web handlers, but can be used in your code to trace coroutines + execution. + """ + with TracerStackContext(): + return func(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/template.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/template.py new file mode 100644 index 000000000..c9e851b53 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/tornado/template.py @@ -0,0 +1,31 @@ +from tornado import template + +from ddtrace import Pin + +from ...ext import SpanTypes + + +def generate(func, renderer, args, kwargs): + """ + Wrap the ``generate`` method used in templates rendering. Because the method + may be called everywhere, the execution is traced in a tracer StackContext that + inherits the current one if it's already available. + """ + # get the module pin + pin = Pin.get_from(template) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + # change the resource and the template name + # if it's created from a string instead of a file + if "" in renderer.name: + resource = template_name = "render_string" + else: + resource = template_name = renderer.name + + # trace the original call + with pin.tracer.trace( + "tornado.template", service=pin.service, resource=resource, span_type=SpanTypes.TEMPLATE + ) as span: + span.set_meta("tornado.template_name", template_name) + return func(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/trace_utils.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/trace_utils.py new file mode 100644 index 000000000..e6b2ce885 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/trace_utils.py @@ -0,0 +1,323 @@ +""" +This module contains utility functions for writing ddtrace integrations. +""" +from collections import deque +import re +from typing import Any +from typing import Callable +from typing import Dict +from typing import Generator +from typing import Iterator +from typing import Optional +from typing import TYPE_CHECKING +from typing import Tuple + +from ddtrace import Pin +from ddtrace import config +from ddtrace.ext import http +from ddtrace.internal.logger import get_logger +from ddtrace.propagation.http import HTTPPropagator +from ddtrace.utils.cache import cached +from ddtrace.utils.http import normalize_header_name +from ddtrace.utils.http import strip_query_string +import ddtrace.utils.wrappers +from ddtrace.vendor import wrapt + + +if TYPE_CHECKING: + from ddtrace import Span + from ddtrace import Tracer + from ddtrace.settings import IntegrationConfig + + +log = get_logger(__name__) + +wrap = wrapt.wrap_function_wrapper +unwrap = ddtrace.utils.wrappers.unwrap +iswrapped = ddtrace.utils.wrappers.iswrapped + +REQUEST = "request" +RESPONSE = "response" + +# Tag normalization based on: https://docs.datadoghq.com/tagging/#defining-tags +# With the exception of '.' in header names which are replaced with '_' to avoid +# starting a "new object" on the UI. +NORMALIZE_PATTERN = re.compile(r"([^a-z0-9_\-:/]){1}") + + +@cached() +def _normalized_header_name(header_name): + # type: (str) -> str + return NORMALIZE_PATTERN.sub("_", normalize_header_name(header_name)) + + +def _normalize_tag_name(request_or_response, header_name): + # type: (str, str) -> str + """ + Given a tag name, e.g. 'Content-Type', returns a corresponding normalized tag name, i.e + 'http.request.headers.content_type'. Rules applied actual header name are: + - any letter is converted to lowercase + - any digit is left unchanged + - any block of any length of different ASCII chars is converted to a single underscore '_' + :param request_or_response: The context of the headers: request|response + :param header_name: The header's name + :type header_name: str + :rtype: str + """ + # Looking at: + # - http://www.iana.org/assignments/message-headers/message-headers.xhtml + # - https://tools.ietf.org/html/rfc6648 + # and for consistency with other language integrations seems safe to assume the following algorithm for header + # names normalization: + # - any letter is converted to lowercase + # - any digit is left unchanged + # - any block of any length of different ASCII chars is converted to a single underscore '_' + normalized_name = _normalized_header_name(header_name) + return "http.{}.headers.{}".format(request_or_response, normalized_name) + + +def _store_headers(headers, span, integration_config, request_or_response): + # type: (Dict[str, str], Span, IntegrationConfig, str) -> None + """ + :param headers: A dict of http headers to be stored in the span + :type headers: dict or list + :param span: The Span instance where tags will be stored + :type span: ddtrace.span.Span + :param integration_config: An integration specific config object. + :type integration_config: ddtrace.settings.IntegrationConfig + """ + if not isinstance(headers, dict): + try: + headers = dict(headers) + except Exception: + return + + if integration_config is None: + log.debug("Skipping headers tracing as no integration config was provided") + return + + for header_name, header_value in headers.items(): + tag_name = integration_config._header_tag_name(header_name) + if tag_name is None: + continue + # An empty tag defaults to a http..headers.
tag + span.set_tag(tag_name or _normalize_tag_name(request_or_response, header_name), header_value) + + +def _store_request_headers(headers, span, integration_config): + # type: (Dict[str, str], Span, IntegrationConfig) -> None + """ + Store request headers as a span's tags + :param headers: All the request's http headers, will be filtered through the whitelist + :type headers: dict or list + :param span: The Span instance where tags will be stored + :type span: ddtrace.Span + :param integration_config: An integration specific config object. + :type integration_config: ddtrace.settings.IntegrationConfig + """ + _store_headers(headers, span, integration_config, REQUEST) + + +def _store_response_headers(headers, span, integration_config): + # type: (Dict[str, str], Span, IntegrationConfig) -> None + """ + Store response headers as a span's tags + :param headers: All the response's http headers, will be filtered through the whitelist + :type headers: dict or list + :param span: The Span instance where tags will be stored + :type span: ddtrace.Span + :param integration_config: An integration specific config object. + :type integration_config: ddtrace.settings.IntegrationConfig + """ + _store_headers(headers, span, integration_config, RESPONSE) + + +def with_traced_module(func): + """Helper for providing tracing essentials (module and pin) for tracing + wrappers. + + This helper enables tracing wrappers to dynamically be disabled when the + corresponding pin is disabled. + + Usage:: + + @with_traced_module + def my_traced_wrapper(django, pin, func, instance, args, kwargs): + # Do tracing stuff + pass + + def patch(): + import django + wrap(django.somefunc, my_traced_wrapper(django)) + """ + + def with_mod(mod): + def wrapper(wrapped, instance, args, kwargs): + pin = Pin._find(instance, mod) + if pin and not pin.enabled(): + return wrapped(*args, **kwargs) + elif not pin: + log.debug("Pin not found for traced method %r", wrapped) + return wrapped(*args, **kwargs) + return func(mod, pin, wrapped, instance, args, kwargs) + + return wrapper + + return with_mod + + +def distributed_tracing_enabled(int_config, default=False): + # type: (IntegrationConfig, bool) -> bool + """Returns whether distributed tracing is enabled for this integration config""" + if "distributed_tracing_enabled" in int_config and int_config.distributed_tracing_enabled is not None: + return int_config.distributed_tracing_enabled + elif "distributed_tracing" in int_config and int_config.distributed_tracing is not None: + return int_config.distributed_tracing + return default + + +def int_service(pin, int_config, default=None): + """Returns the service name for an integration which is internal + to the application. Internal meaning that the work belongs to the + user's application. Eg. Web framework, sqlalchemy, web servers. + + For internal integrations we prioritize overrides, then global defaults and + lastly the default provided by the integration. + """ + int_config = int_config or {} + + # Pin has top priority since it is user defined in code + if pin and pin.service: + return pin.service + + # Config is next since it is also configured via code + # Note that both service and service_name are used by + # integrations. + if "service" in int_config and int_config.service is not None: + return int_config.service + if "service_name" in int_config and int_config.service_name is not None: + return int_config.service_name + + global_service = int_config.global_config._get_service() + if global_service: + return global_service + + if "_default_service" in int_config and int_config._default_service is not None: + return int_config._default_service + + return default + + +def ext_service(pin, int_config, default=None): + """Returns the service name for an integration which is external + to the application. External meaning that the integration generates + spans wrapping code that is outside the scope of the user's application. Eg. A database, RPC, cache, etc. + """ + int_config = int_config or {} + + if pin and pin.service: + return pin.service + + if "service" in int_config and int_config.service is not None: + return int_config.service + if "service_name" in int_config and int_config.service_name is not None: + return int_config.service_name + + if "_default_service" in int_config and int_config._default_service is not None: + return int_config._default_service + + # A default is required since it's an external service. + return default + + +def set_http_meta( + span, + integration_config, + method=None, + url=None, + status_code=None, + status_msg=None, + query=None, + request_headers=None, + response_headers=None, + retries_remain=None, +): + if method is not None: + span._set_str_tag(http.METHOD, method) + + if url is not None: + span._set_str_tag(http.URL, url if integration_config.trace_query_string else strip_query_string(url)) + + if status_code is not None: + try: + int_status_code = int(status_code) + except (TypeError, ValueError): + log.debug("failed to convert http status code %r to int", status_code) + else: + span._set_str_tag(http.STATUS_CODE, str(status_code)) + if config.http_server.is_error_code(int_status_code): + span.error = 1 + + if status_msg is not None: + span._set_str_tag(http.STATUS_MSG, status_msg) + + if query is not None and integration_config.trace_query_string: + span._set_str_tag(http.QUERY_STRING, query) + + if request_headers is not None and integration_config.is_header_tracing_configured: + _store_request_headers(dict(request_headers), span, integration_config) + + if response_headers is not None and integration_config.is_header_tracing_configured: + _store_response_headers(dict(response_headers), span, integration_config) + + if retries_remain is not None: + span._set_str_tag(http.RETRIES_REMAIN, str(retries_remain)) + + +def activate_distributed_headers(tracer, int_config=None, request_headers=None, override=None): + # type: (Tracer, Optional[IntegrationConfig], Optional[Dict[str, str]], Optional[bool]) -> None + """ + Helper for activating a distributed trace headers' context if enabled in integration config. + int_config will be used to check if distributed trace headers context will be activated, but + override will override whatever value is set in int_config if passed any value other than None. + """ + if override is False: + return None + + if override or (int_config and distributed_tracing_enabled(int_config)): + context = HTTPPropagator.extract(request_headers) + # Only need to activate the new context if something was propagated + if context.trace_id: + tracer.context_provider.activate(context) + + +def _flatten( + obj, # type: Any + sep=".", # type: str + prefix="", # type: str + exclude_policy=None, # type: Optional[Callable[[str], bool]] +): + # type: (...) -> Generator[Tuple[str, Any], None, None] + s = deque() # type: ignore + s.append((prefix, obj)) + while s: + p, v = s.pop() + if exclude_policy is not None and exclude_policy(p): + continue + if isinstance(v, dict): + s.extend((sep.join((p, k)) if p else k, v) for k, v in v.items()) + else: + yield p, v + + +def set_flattened_tags( + span, # type: Span + items, # type: Iterator[Tuple[str, Any]] + sep=".", # type: str + exclude_policy=None, # type: Optional[Callable[[str], bool]] + processor=None, # type: Optional[Callable[[Any], Any]] +): + # type: (...) -> None + for prefix, value in items: + for tag, v in _flatten(value, sep, prefix, exclude_policy): + span.set_tag(tag, processor(v) if processor is not None else v) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/urllib3/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/urllib3/__init__.py new file mode 100644 index 000000000..3d202e067 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/urllib3/__init__.py @@ -0,0 +1,66 @@ +""" +The ``urllib3`` integration instruments tracing on http calls with optional +support for distributed tracing across services the client communicates with. + + +Enabling +~~~~~~~~ + +The ``urllib3`` integration is not enabled by default. Use ``patch_all()`` +with the environment variable ``DD_TRACE_URLLIB3_ENABLED`` set, or call +:ref:`patch()` with the ``urllib3`` argument set to ``True`` to manually +enable the integration, before importing and using ``urllib3``:: + + from ddtrace import patch + patch(urllib3=True) + + # use urllib3 like usual + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.urllib3['service'] + + The service name reported by default for urllib3 client instances. + + This option can also be set with the ``DD_URLLIB3_SERVICE`` environment + variable. + + Default: ``"urllib3"`` + + +.. py:data:: ddtrace.config.urllib3['distributed_tracing'] + + Whether or not to parse distributed tracing headers. + + Default: ``True`` + + +.. py:data:: ddtrace.config.urllib3['trace_query_string'] + + Whether or not to include the query string as a tag. + + Default: ``False`` + + +.. py:data:: ddtrace.config.urllib3['split_by_domain'] + + Whether or not to use the domain name of requests as the service name. + + Default: ``False`` +""" +from ...utils.importlib import require_modules +from .patch import patch +from .patch import unpatch + + +required_modules = ["urllib3"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + + __all__ = [ + "patch", + "unpatch", + ] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/urllib3/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/urllib3/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..51f4ed10e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/urllib3/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/urllib3/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/urllib3/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..b3118d0b3 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/urllib3/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/urllib3/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/urllib3/patch.py new file mode 100644 index 000000000..ef477ebd6 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/urllib3/patch.py @@ -0,0 +1,129 @@ +import urllib3 + +from ddtrace import config +from ddtrace.pin import Pin +from ddtrace.vendor.wrapt import wrap_function_wrapper as _w + +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...ext import SpanTypes +from ...internal.compat import parse +from ...propagation.http import HTTPPropagator +from ...utils import ArgumentError +from ...utils import get_argument_value +from ...utils.formats import asbool +from ...utils.formats import get_env +from ...utils.wrappers import unwrap as _u + + +# Ports which, if set, will not be used in hostnames/service names +DROP_PORTS = (80, 443) + +# Initialize the default config vars +config._add( + "urllib3", + { + "_default_service": "urllib3", + "distributed_tracing": asbool(get_env("urllib3", "distributed_tracing", default=True)), + "split_by_domain": asbool(get_env("urllib3", "split_by_domain", default=False)), + }, +) + + +def patch(): + """Enable tracing for all urllib3 requests""" + if getattr(urllib3, "__datadog_patch", False): + return + setattr(urllib3, "__datadog_patch", True) + + _w("urllib3", "connectionpool.HTTPConnectionPool.urlopen", _wrap_urlopen) + Pin().onto(urllib3.connectionpool.HTTPConnectionPool) + + +def unpatch(): + """Disable trace for all urllib3 requests""" + if getattr(urllib3, "__datadog_patch", False): + setattr(urllib3, "__datadog_patch", False) + + _u(urllib3.connectionpool.HTTPConnectionPool, "urlopen") + + +def _wrap_urlopen(func, instance, args, kwargs): + """ + Wrapper function for the lower-level urlopen in urllib3 + + :param func: The original target function "urlopen" + :param instance: The patched instance of ``HTTPConnectionPool`` + :param args: Positional arguments from the target function + :param kwargs: Keyword arguments from the target function + :return: The ``HTTPResponse`` from the target function + """ + request_method = get_argument_value(args, kwargs, 0, "method") + request_url = get_argument_value(args, kwargs, 1, "url") + try: + request_headers = get_argument_value(args, kwargs, 3, "headers") + except ArgumentError: + request_headers = None + try: + request_retries = get_argument_value(args, kwargs, 4, "retries") + except ArgumentError: + request_retries = None + + # HTTPConnectionPool allows relative path requests; convert the request_url to an absolute url + if request_url.startswith("/"): + request_url = parse.urlunparse( + ( + instance.scheme, + "{}:{}".format(instance.host, instance.port) + if instance.port and instance.port not in DROP_PORTS + else str(instance.host), + request_url, + None, + None, + None, + ) + ) + + parsed_uri = parse.urlparse(request_url) + hostname = parsed_uri.netloc + + pin = Pin.get_from(instance) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + with pin.tracer.trace( + "urllib3.request", service=trace_utils.ext_service(pin, config.urllib3), span_type=SpanTypes.HTTP + ) as span: + if config.urllib3.split_by_domain: + span.service = hostname + + # If distributed tracing is enabled, propagate the tracing headers to downstream services + if config.urllib3.distributed_tracing: + if request_headers is None: + request_headers = {} + kwargs["headers"] = request_headers + HTTPPropagator.inject(span.context, request_headers) + + if config.urllib3.analytics_enabled: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.urllib3.get_analytics_sample_rate()) + + retries = request_retries.total if isinstance(request_retries, urllib3.util.retry.Retry) else None + + # Call the target function + response = None + try: + response = func(*args, **kwargs) + finally: + trace_utils.set_http_meta( + span, + integration_config=config.urllib3, + method=request_method, + url=request_url, + status_code=None if response is None else response.status, + query=parsed_uri.query, + request_headers=request_headers, + response_headers={} if response is None else dict(response.headers), + retries_remain=retries, + ) + + return response diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/util.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/util.py new file mode 100644 index 000000000..3a4174497 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/util.py @@ -0,0 +1,18 @@ +# [Backward compatibility]: keep importing modules functions +from ..utils.deprecation import deprecation +from ..utils.importlib import func_name +from ..utils.importlib import module_name +from ..utils.importlib import require_modules + + +deprecation( + name="ddtrace.contrib.util", + message="Use `ddtrace.utils.importlib` module instead", + version="1.0.0", +) + +__all__ = [ + "require_modules", + "func_name", + "module_name", +] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/__init__.py new file mode 100644 index 000000000..d2f60f443 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/__init__.py @@ -0,0 +1,52 @@ +""" +The Vertica integration will trace queries made using the vertica-python +library. + +Vertica will be automatically instrumented with ``patch_all``, or when using +the ``ddtrace-run`` command. + +Vertica is instrumented on import. To instrument Vertica manually use the +``patch`` function. Note the ordering of the following statements:: + + from ddtrace import patch + patch(vertica=True) + + import vertica_python + + # use vertica_python like usual + + +To configure the Vertica integration globally you can use the ``Config`` API:: + + from ddtrace import config, patch + patch(vertica=True) + + config.vertica['service_name'] = 'my-vertica-database' + + +To configure the Vertica integration on an instance-per-instance basis use the +``Pin`` API:: + + from ddtrace import Pin, patch, Tracer + patch(vertica=True) + + import vertica_python + + custom_tracer = Tracer() + conn = vertica_python.connect(**YOUR_VERTICA_CONFIG) + + # override the service and tracer to be used + Pin.override(conn, service='myverticaservice', tracer=custom_tracer) +""" + +from ...utils.importlib import require_modules + + +required_modules = ["vertica_python"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = ["patch", "unpatch"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..dbde0bb28 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/__pycache__/constants.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/__pycache__/constants.cpython-38.pyc new file mode 100644 index 000000000..28b640fbc Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/__pycache__/constants.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/__pycache__/patch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/__pycache__/patch.cpython-38.pyc new file mode 100644 index 000000000..5f84a6da5 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/__pycache__/patch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/constants.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/constants.py new file mode 100644 index 000000000..a44b81be4 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/constants.py @@ -0,0 +1,2 @@ +# Service info +APP = "vertica" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/patch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/patch.py new file mode 100644 index 000000000..e89033d28 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/vertica/patch.py @@ -0,0 +1,243 @@ +import importlib + +import ddtrace +from ddtrace import config +from ddtrace.vendor import wrapt + +from .. import trace_utils +from ...constants import ANALYTICS_SAMPLE_RATE_KEY +from ...constants import SPAN_MEASURED_KEY +from ...ext import SpanTypes +from ...ext import db as dbx +from ...ext import net +from ...internal.logger import get_logger +from ...pin import Pin +from ...utils import get_argument_value +from ...utils.wrappers import unwrap +from .constants import APP + + +log = get_logger(__name__) + +_PATCHED = False + + +def copy_span_start(instance, span, conf, *args, **kwargs): + span.resource = get_argument_value(args, kwargs, 0, "sql") + + +def execute_span_start(instance, span, conf, *args, **kwargs): + span.resource = get_argument_value(args, kwargs, 0, "operation") + + +def execute_span_end(instance, result, span, conf, *args, **kwargs): + span.set_metric(dbx.ROWCOUNT, instance.rowcount) + + +def fetch_span_end(instance, result, span, conf, *args, **kwargs): + span.set_metric(dbx.ROWCOUNT, instance.rowcount) + + +def cursor_span_end(instance, cursor, _, conf, *args, **kwargs): + tags = {} + tags[net.TARGET_HOST] = instance.options["host"] + tags[net.TARGET_PORT] = instance.options["port"] + if "user" in instance.options: + tags[dbx.USER] = instance.options["user"] + if "database" in instance.options: + tags[dbx.NAME] = instance.options["database"] + + pin = Pin( + app=APP, + tags=tags, + _config=config.vertica["patch"]["vertica_python.vertica.cursor.Cursor"], + ) + pin.onto(cursor) + + +# tracing configuration +config._add( + "vertica", + { + "_default_service": "vertica", + "app": "vertica", + "patch": { + "vertica_python.vertica.connection.Connection": { + "routines": { + "cursor": { + "trace_enabled": False, + "span_end": cursor_span_end, + }, + }, + }, + "vertica_python.vertica.cursor.Cursor": { + "routines": { + "execute": { + "operation_name": "vertica.query", + "span_type": SpanTypes.SQL, + "span_start": execute_span_start, + "span_end": execute_span_end, + "measured": True, + }, + "copy": { + "operation_name": "vertica.copy", + "span_type": SpanTypes.SQL, + "span_start": copy_span_start, + "measured": False, + }, + "fetchone": { + "operation_name": "vertica.fetchone", + "span_type": SpanTypes.SQL, + "span_end": fetch_span_end, + "measured": False, + }, + "fetchall": { + "operation_name": "vertica.fetchall", + "span_type": SpanTypes.SQL, + "span_end": fetch_span_end, + "measured": False, + }, + "nextset": { + "operation_name": "vertica.nextset", + "span_type": SpanTypes.SQL, + "span_end": fetch_span_end, + "measured": False, + }, + }, + }, + }, + }, +) + + +def patch(): + global _PATCHED + if _PATCHED: + return + + _install(config.vertica) + _PATCHED = True + + +def unpatch(): + global _PATCHED + if _PATCHED: + _uninstall(config.vertica) + _PATCHED = False + + +def _uninstall(config): + for patch_class_path in config["patch"]: + patch_mod, _, patch_class = patch_class_path.rpartition(".") + mod = importlib.import_module(patch_mod) + cls = getattr(mod, patch_class, None) + + if not cls: + log.debug( + """ + Unable to find corresponding class for tracing configuration. + This version may not be supported. + """ + ) + continue + + for patch_routine in config["patch"][patch_class_path]["routines"]: + unwrap(cls, patch_routine) + + +def _find_routine_config(config, instance, routine_name): + """Attempts to find the config for a routine based on the bases of the + class of the instance. + """ + bases = instance.__class__.__mro__ + for base in bases: + full_name = "{}.{}".format(base.__module__, base.__name__) + if full_name not in config["patch"]: + continue + + config_routines = config["patch"][full_name]["routines"] + + if routine_name in config_routines: + return config_routines[routine_name] + return {} + + +def _install_init(patch_item, patch_class, patch_mod, config): + patch_class_routine = "{}.{}".format(patch_class, "__init__") + + # patch the __init__ of the class with a Pin instance containing the defaults + @wrapt.patch_function_wrapper(patch_mod, patch_class_routine) + def init_wrapper(wrapped, instance, args, kwargs): + r = wrapped(*args, **kwargs) + + # create and attach a pin with the defaults + Pin( + app=config["app"], + tags=config.get("tags", {}), + tracer=config.get("tracer", ddtrace.tracer), + _config=config["patch"][patch_item], + ).onto(instance) + return r + + +def _install_routine(patch_routine, patch_class, patch_mod, config): + patch_class_routine = "{}.{}".format(patch_class, patch_routine) + + @wrapt.patch_function_wrapper(patch_mod, patch_class_routine) + def wrapper(wrapped, instance, args, kwargs): + # TODO?: remove Pin dependence + pin = Pin.get_from(instance) + + if patch_routine in pin._config["routines"]: + conf = pin._config["routines"][patch_routine] + else: + conf = _find_routine_config(config, instance, patch_routine) + + enabled = conf.get("trace_enabled", True) + + span = None + + try: + # shortcut if not enabled + if not enabled: + result = wrapped(*args, **kwargs) + return result + + operation_name = conf["operation_name"] + tracer = pin.tracer + with tracer.trace( + operation_name, + service=trace_utils.ext_service(pin, config), + span_type=conf.get("span_type"), + ) as span: + if conf.get("measured", False): + span.set_tag(SPAN_MEASURED_KEY) + span.set_tags(pin.tags) + + if "span_start" in conf: + conf["span_start"](instance, span, conf, *args, **kwargs) + + # set analytics sample rate + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, config.get_analytics_sample_rate()) + + result = wrapped(*args, **kwargs) + return result + except Exception as err: + if "on_error" in conf: + conf["on_error"](instance, err, span, conf, *args, **kwargs) + raise + finally: + # if an exception is raised result will not exist + if "result" not in locals(): + result = None + if "span_end" in conf: + conf["span_end"](instance, result, span, conf, *args, **kwargs) + + +def _install(config): + for patch_class_path in config["patch"]: + patch_mod, _, patch_class = patch_class_path.rpartition(".") + _install_init(patch_class_path, patch_class, patch_mod, config) + + for patch_routine in config["patch"][patch_class_path]["routines"]: + _install_routine(patch_routine, patch_class, patch_mod, config) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/wsgi/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/wsgi/__init__.py new file mode 100644 index 000000000..d40db1376 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/wsgi/__init__.py @@ -0,0 +1,44 @@ +""" +The Datadog WSGI middleware traces all WSGI requests. + + +Usage +~~~~~ + +The middleware can be used manually via the following command:: + + + from ddtrace.contrib.wsgi import DDWSGIMiddleware + + # application is a WSGI application + application = DDWSGIMiddleware(application) + + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.wsgi["service"] + + The service name reported for the WSGI application. + + This option can also be set with the ``DD_SERVICE`` environment + variable. + + Default: ``"wsgi"`` + +.. py:data:: ddtrace.config.wsgi["distributed_tracing"] + + Configuration that allows distributed tracing to be enabled. + + Default: ``True`` + + +:ref:`All HTTP tags ` are supported for this integration. + +""" +from .wsgi import DDWSGIMiddleware + + +__all__ = [ + "DDWSGIMiddleware", +] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/wsgi/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/wsgi/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..320d866c1 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/wsgi/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/wsgi/__pycache__/wsgi.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/wsgi/__pycache__/wsgi.cpython-38.pyc new file mode 100644 index 000000000..bb1131851 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/wsgi/__pycache__/wsgi.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/wsgi/wsgi.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/wsgi/wsgi.py new file mode 100644 index 000000000..f3edd0a65 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/contrib/wsgi/wsgi.py @@ -0,0 +1,152 @@ +import sys + +from ddtrace.internal.compat import PY2 + + +if PY2: + import exceptions + + generatorExit = exceptions.GeneratorExit +else: + import builtins + + generatorExit = builtins.GeneratorExit + + +import six +from six.moves.urllib.parse import quote + +import ddtrace +from ddtrace import config +from ddtrace.ext import SpanTypes +from ddtrace.internal.logger import get_logger +from ddtrace.propagation.http import HTTPPropagator +from ddtrace.propagation.utils import from_wsgi_header + +from .. import trace_utils + + +log = get_logger(__name__) + +propagator = HTTPPropagator + +config._add( + "wsgi", + dict( + _default_service="wsgi", + distributed_tracing=True, + ), +) + + +def construct_url(environ): + """ + https://www.python.org/dev/peps/pep-3333/#url-reconstruction + """ + url = environ["wsgi.url_scheme"] + "://" + + if environ.get("HTTP_HOST"): + url += environ["HTTP_HOST"] + else: + url += environ["SERVER_NAME"] + + if environ["wsgi.url_scheme"] == "https": + if environ["SERVER_PORT"] != "443": + url += ":" + environ["SERVER_PORT"] + else: + if environ["SERVER_PORT"] != "80": + url += ":" + environ["SERVER_PORT"] + + url += quote(environ.get("SCRIPT_NAME", "")) + url += quote(environ.get("PATH_INFO", "")) + if environ.get("QUERY_STRING"): + url += "?" + environ["QUERY_STRING"] + + return url + + +def get_request_headers(environ): + """ + Manually grab the request headers from the environ dictionary. + """ + request_headers = {} + for key in environ.keys(): + if key.startswith("HTTP"): + request_headers[from_wsgi_header(key)] = environ[key] + return request_headers + + +def default_wsgi_span_modifier(span, environ): + span.resource = "{} {}".format(environ["REQUEST_METHOD"], environ["PATH_INFO"]) + + +class DDWSGIMiddleware(object): + """WSGI middleware providing tracing around an application. + + :param application: The WSGI application to apply the middleware to. + :param tracer: Tracer instance to use the middleware with. Defaults to the global tracer. + :param span_modifier: Span modifier that can add tags to the root span. + Defaults to using the request method and url in the resource. + """ + + def __init__(self, application, tracer=None, span_modifier=default_wsgi_span_modifier): + self.app = application + self.tracer = tracer or ddtrace.tracer + self.span_modifier = span_modifier + + def __call__(self, environ, start_response): + def intercept_start_response(status, response_headers, exc_info=None): + span = self.tracer.current_root_span() + if span is not None: + status_code, status_msg = status.split(" ", 1) + span.set_tag("http.status_msg", status_msg) + trace_utils.set_http_meta(span, config.wsgi, status_code=status_code, response_headers=response_headers) + with self.tracer.trace( + "wsgi.start_response", + service=trace_utils.int_service(None, config.wsgi), + span_type=SpanTypes.WEB, + ): + write = start_response(status, response_headers, exc_info) + return write + + trace_utils.activate_distributed_headers(self.tracer, int_config=config.wsgi, request_headers=environ) + + with self.tracer.trace( + "wsgi.request", + service=trace_utils.int_service(None, config.wsgi), + span_type=SpanTypes.WEB, + ) as span: + # This prevents GeneratorExit exceptions from being propagated to the top-level span. + # This can occur if a streaming response exits abruptly leading to a broken pipe. + # Note: The wsgi.response span will still have the error information included. + span._ignore_exception(generatorExit) + with self.tracer.trace("wsgi.application"): + result = self.app(environ, intercept_start_response) + + with self.tracer.trace("wsgi.response") as resp_span: + if hasattr(result, "__class__"): + resp_class = getattr(getattr(result, "__class__"), "__name__", None) + if resp_class: + resp_span.meta["result_class"] = resp_class + + for chunk in result: + yield chunk + + url = construct_url(environ) + method = environ.get("REQUEST_METHOD") + query_string = environ.get("QUERY_STRING") + request_headers = get_request_headers(environ) + trace_utils.set_http_meta( + span, config.wsgi, method=method, url=url, query=query_string, request_headers=request_headers + ) + + if self.span_modifier: + self.span_modifier(span, environ) + + if hasattr(result, "close"): + try: + result.close() + except Exception: + typ, val, tb = sys.exc_info() + span.set_exc_info(typ, val, tb) + six.reraise(typ, val, tb=tb) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/encoding.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/encoding.py new file mode 100644 index 000000000..a3586feaf --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/encoding.py @@ -0,0 +1,20 @@ +from .internal.encoding import Encoder +from .internal.encoding import JSONEncoder +from .internal.encoding import JSONEncoderV2 +from .internal.encoding import MsgpackEncoderV03 as MsgpackEncoder +from .utils.deprecation import deprecation + + +__all__ = ( + "Encoder", + "JSONEncoder", + "JSONEncoderV2", + "MsgpackEncoder", +) + + +deprecation( + name="ddtrace.encoding", + message="The encoding module has been moved to ddtrace.internal and will no longer be part of the public API.", + version="1.0.0", +) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__init__.py new file mode 100644 index 000000000..b3493eb52 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__init__.py @@ -0,0 +1,16 @@ +from enum import Enum + + +class SpanTypes(Enum): + CACHE = "cache" + CASSANDRA = "cassandra" + ELASTICSEARCH = "elasticsearch" + GRPC = "grpc" + HTTP = "http" + MONGODB = "mongodb" + REDIS = "redis" + SQL = "sql" + TEMPLATE = "template" + TEST = "test" + WEB = "web" + WORKER = "worker" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..6dead66b5 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/aws.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/aws.cpython-38.pyc new file mode 100644 index 000000000..eaa0473d4 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/aws.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/cassandra.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/cassandra.cpython-38.pyc new file mode 100644 index 000000000..ad103130f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/cassandra.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/ci.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/ci.cpython-38.pyc new file mode 100644 index 000000000..0f6e813c6 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/ci.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/consul.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/consul.cpython-38.pyc new file mode 100644 index 000000000..dc1975729 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/consul.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/db.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/db.cpython-38.pyc new file mode 100644 index 000000000..d8818cc89 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/db.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/elasticsearch.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/elasticsearch.cpython-38.pyc new file mode 100644 index 000000000..44829c027 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/elasticsearch.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/errors.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/errors.cpython-38.pyc new file mode 100644 index 000000000..2e2b24dad Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/errors.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/git.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/git.cpython-38.pyc new file mode 100644 index 000000000..f3044797c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/git.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/http.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/http.cpython-38.pyc new file mode 100644 index 000000000..487700560 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/http.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/kombu.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/kombu.cpython-38.pyc new file mode 100644 index 000000000..671fd809f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/kombu.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/memcached.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/memcached.cpython-38.pyc new file mode 100644 index 000000000..8126cd803 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/memcached.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/mongo.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/mongo.cpython-38.pyc new file mode 100644 index 000000000..fc527c90a Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/mongo.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/net.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/net.cpython-38.pyc new file mode 100644 index 000000000..dd1964f93 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/net.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/priority.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/priority.cpython-38.pyc new file mode 100644 index 000000000..45513ac71 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/priority.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/redis.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/redis.cpython-38.pyc new file mode 100644 index 000000000..1cc26c570 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/redis.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/sql.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/sql.cpython-38.pyc new file mode 100644 index 000000000..7652b424f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/sql.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/system.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/system.cpython-38.pyc new file mode 100644 index 000000000..056d490c1 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/system.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/test.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/test.cpython-38.pyc new file mode 100644 index 000000000..d0c3797ff Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/__pycache__/test.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/aws.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/aws.py new file mode 100644 index 000000000..08d1ba92e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/aws.py @@ -0,0 +1,51 @@ +from typing import Any +from typing import FrozenSet +from typing import Set +from typing import TYPE_CHECKING +from typing import Tuple + +from ddtrace.contrib.trace_utils import set_flattened_tags + + +if TYPE_CHECKING: + from ddtrace.span import Span + + +EXCLUDED_ENDPOINT = frozenset({"kms", "sts"}) +EXCLUDED_ENDPOINT_TAGS = { + "firehose": frozenset({"params.Records"}), +} + + +def truncate_arg_value(value, max_len=1024): + # type: (Any, int) -> Any + """Truncate values which are bytes and greater than `max_len`. + Useful for parameters like 'Body' in `put_object` operations. + """ + if isinstance(value, bytes) and len(value) > max_len: + return b"..." + + return value + + +def add_span_arg_tags( + span, # type: Span + endpoint_name, # type: str + args, # type: Tuple[Any] + args_names, # type: Tuple[str] + args_traced, # type: Set[str] +): + # type: (...) -> None + if endpoint_name not in EXCLUDED_ENDPOINT: + exclude_set = EXCLUDED_ENDPOINT_TAGS.get(endpoint_name, frozenset()) # type: FrozenSet[str] + set_flattened_tags( + span, + items=((name, value) for (name, value) in zip(args_names, args) if name in args_traced), + exclude_policy=lambda tag: tag in exclude_set or tag.endswith("Body"), + processor=truncate_arg_value, + ) + + +REGION = "aws.region" +AGENT = "aws.agent" +OPERATION = "aws.operation" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/cassandra.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/cassandra.py new file mode 100644 index 000000000..88c03e121 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/cassandra.py @@ -0,0 +1,7 @@ +# tags +CLUSTER = "cassandra.cluster" +KEYSPACE = "cassandra.keyspace" +CONSISTENCY_LEVEL = "cassandra.consistency_level" +PAGINATED = "cassandra.paginated" +ROW_COUNT = "cassandra.row_count" +PAGE_NUMBER = "cassandra.page_number" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/ci.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/ci.py new file mode 100644 index 000000000..e330609e0 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/ci.py @@ -0,0 +1,446 @@ +""" +Tags for common CI attributes +""" +import os +import platform +import re +from typing import Dict +from typing import MutableMapping +from typing import Optional + +from ddtrace.ext import git +from ddtrace.internal.logger import get_logger + + +# CI app dd_origin tag +CI_APP_TEST_ORIGIN = "ciapp-test" + +# Stage Name +STAGE_NAME = "ci.stage.name" + +# Job Name +JOB_NAME = "ci.job.name" + +# Job URL +JOB_URL = "ci.job.url" + +# Pipeline ID +PIPELINE_ID = "ci.pipeline.id" + +# Pipeline Name +PIPELINE_NAME = "ci.pipeline.name" + +# Pipeline Number +PIPELINE_NUMBER = "ci.pipeline.number" + +# Pipeline URL +PIPELINE_URL = "ci.pipeline.url" + +# Provider +PROVIDER_NAME = "ci.provider.name" + +# Workspace Path +WORKSPACE_PATH = "ci.workspace_path" + +# Architecture +OS_ARCHITECTURE = "os.architecture" + +# Platform +OS_PLATFORM = "os.platform" + +# Version +OS_VERSION = "os.version" + +# Runtime Name +RUNTIME_NAME = "runtime.name" + +# Runtime Version +RUNTIME_VERSION = "runtime.version" + +_RE_REFS = re.compile(r"^refs/(heads/)?") +_RE_ORIGIN = re.compile(r"^origin/") +_RE_TAGS = re.compile(r"^tags/") +_RE_URL = re.compile(r"(https?://)[^/]*@") + + +log = get_logger(__name__) + + +def _normalize_ref(name): + # type: (Optional[str]) -> Optional[str] + return _RE_TAGS.sub("", _RE_ORIGIN.sub("", _RE_REFS.sub("", name))) if name is not None else None + + +def _filter_sensitive_info(url): + # type: (Optional[str]) -> Optional[str] + return _RE_URL.sub("\\1", url) if url is not None else None + + +def _get_runtime_and_os_metadata(): + """Extract configuration facet tags for OS and Python runtime.""" + return { + OS_ARCHITECTURE: platform.machine(), + OS_PLATFORM: platform.system(), + OS_VERSION: platform.release(), + RUNTIME_NAME: platform.python_implementation(), + RUNTIME_VERSION: platform.python_version(), + } + + +def tags(env=None, cwd=None): + # type: (Optional[MutableMapping[str, str]], Optional[str]) -> Dict[str, str] + """Extract and set tags from provider environ, as well as git metadata.""" + env = os.environ if env is None else env + tags = {} # type: Dict[str, Optional[str]] + for key, extract in PROVIDERS: + if key in env: + tags = extract(env) + break + + git_info = git.extract_git_metadata(cwd=cwd) + try: + git_info[WORKSPACE_PATH] = git.extract_workspace_path(cwd=cwd) + except git.GitNotFoundError: + log.error("Git executable not found, cannot extract git metadata.") + except ValueError: + log.error("Error extracting git metadata, received non-zero return code.", exc_info=True) + + # Tags collected from CI provider take precedence over extracted git metadata, but any CI provider value + # is None or "" should be overwritten. + tags.update({k: v for k, v in git_info.items() if not tags.get(k)}) + + user_specified_git_info = git.extract_user_git_metadata(env) + + # Tags provided by the user take precedence over everything + tags.update({k: v for k, v in user_specified_git_info.items() if v}) + + tags[git.TAG] = _normalize_ref(tags.get(git.TAG)) + if tags.get(git.TAG) and git.BRANCH in tags: + del tags[git.BRANCH] + tags[git.BRANCH] = _normalize_ref(tags.get(git.BRANCH)) + tags[git.REPOSITORY_URL] = _filter_sensitive_info(tags.get(git.REPOSITORY_URL)) + + workspace_path = tags.get(WORKSPACE_PATH) + if workspace_path: + tags[WORKSPACE_PATH] = os.path.expanduser(workspace_path) + + tags.update(_get_runtime_and_os_metadata()) + + return {k: v for k, v in tags.items() if v is not None} + + +def extract_appveyor(env): + # type: (MutableMapping[str, str]) -> Dict[str, Optional[str]] + """Extract CI tags from Appveyor environ.""" + url = "https://ci.appveyor.com/project/{0}/builds/{1}".format( + env.get("APPVEYOR_REPO_NAME"), env.get("APPVEYOR_BUILD_ID") + ) + if env.get("APPVEYOR_REPO_PROVIDER") == "github": + repository = "https://github.com/{0}.git".format(env.get("APPVEYOR_REPO_NAME")) # type: Optional[str] + commit = env.get("APPVEYOR_REPO_COMMIT") # type: Optional[str] + branch = env.get("APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH") or env.get( + "APPVEYOR_REPO_BRANCH" + ) # type: Optional[str] + tag = env.get("APPVEYOR_REPO_TAG_NAME") # type: Optional[str] + else: + repository = commit = branch = tag = None + + return { + PROVIDER_NAME: "appveyor", + git.REPOSITORY_URL: repository, + git.COMMIT_SHA: commit, + WORKSPACE_PATH: env.get("APPVEYOR_BUILD_FOLDER"), + PIPELINE_ID: env.get("APPVEYOR_BUILD_ID"), + PIPELINE_NAME: env.get("APPVEYOR_REPO_NAME"), + PIPELINE_NUMBER: env.get("APPVEYOR_BUILD_NUMBER"), + PIPELINE_URL: url, + JOB_URL: url, + git.BRANCH: branch, + git.TAG: tag, + git.COMMIT_MESSAGE: env.get("APPVEYOR_REPO_COMMIT_MESSAGE_EXTENDED"), + git.COMMIT_AUTHOR_NAME: env.get("APPVEYOR_REPO_COMMIT_AUTHOR"), + git.COMMIT_AUTHOR_EMAIL: env.get("APPVEYOR_REPO_COMMIT_AUTHOR_EMAIL"), + } + + +def extract_azure_pipelines(env): + # type: (MutableMapping[str, str]) -> Dict[str, Optional[str]] + """Extract CI tags from Azure pipelines environ.""" + if env.get("SYSTEM_TEAMFOUNDATIONSERVERURI") and env.get("SYSTEM_TEAMPROJECTID") and env.get("BUILD_BUILDID"): + base_url = "{0}{1}/_build/results?buildId={2}".format( + env.get("SYSTEM_TEAMFOUNDATIONSERVERURI"), env.get("SYSTEM_TEAMPROJECTID"), env.get("BUILD_BUILDID") + ) + pipeline_url = base_url # type: Optional[str] + job_url = base_url + "&view=logs&j={0}&t={1}".format( + env.get("SYSTEM_JOBID"), env.get("SYSTEM_TASKINSTANCEID") + ) # type: Optional[str] + else: + pipeline_url = job_url = None + + branch_or_tag = ( + env.get("SYSTEM_PULLREQUEST_SOURCEBRANCH") + or env.get("BUILD_SOURCEBRANCH") + or env.get("BUILD_SOURCEBRANCHNAME") + or "" + ) + branch = tag = None # type: Optional[str] + if "tags/" in branch_or_tag: + tag = branch_or_tag + else: + branch = branch_or_tag + + return { + PROVIDER_NAME: "azurepipelines", + WORKSPACE_PATH: env.get("BUILD_SOURCESDIRECTORY"), + PIPELINE_ID: env.get("BUILD_BUILDID"), + PIPELINE_NAME: env.get("BUILD_DEFINITIONNAME"), + PIPELINE_NUMBER: env.get("BUILD_BUILDID"), + PIPELINE_URL: pipeline_url, + JOB_URL: job_url, + git.REPOSITORY_URL: env.get("SYSTEM_PULLREQUEST_SOURCEREPOSITORYURI") or env.get("BUILD_REPOSITORY_URI"), + git.COMMIT_SHA: env.get("SYSTEM_PULLREQUEST_SOURCECOMMITID") or env.get("BUILD_SOURCEVERSION"), + git.BRANCH: branch, + git.TAG: tag, + git.COMMIT_MESSAGE: env.get("BUILD_SOURCEVERSIONMESSAGE"), + git.COMMIT_AUTHOR_NAME: env.get("BUILD_REQUESTEDFORID"), + git.COMMIT_AUTHOR_EMAIL: env.get("BUILD_REQUESTEDFOREMAIL"), + STAGE_NAME: env.get("SYSTEM_STAGEDISPLAYNAME"), + JOB_NAME: env.get("SYSTEM_JOBDISPLAYNAME"), + } + + +def extract_bitbucket(env): + # type: (MutableMapping[str, str]) -> Dict[str, Optional[str]] + """Extract CI tags from Bitbucket environ.""" + url = "https://bitbucket.org/{0}/addon/pipelines/home#!/results/{1}".format( + env.get("BITBUCKET_REPO_FULL_NAME"), env.get("BITBUCKET_BUILD_NUMBER") + ) + return { + git.BRANCH: env.get("BITBUCKET_BRANCH"), + git.COMMIT_SHA: env.get("BITBUCKET_COMMIT"), + git.REPOSITORY_URL: env.get("BITBUCKET_GIT_SSH_ORIGIN"), + git.TAG: env.get("BITBUCKET_TAG"), + JOB_URL: url, + PIPELINE_ID: env.get("BITBUCKET_PIPELINE_UUID", "").strip("{}}") or None, + PIPELINE_NAME: env.get("BITBUCKET_REPO_FULL_NAME"), + PIPELINE_NUMBER: env.get("BITBUCKET_BUILD_NUMBER"), + PIPELINE_URL: url, + PROVIDER_NAME: "bitbucket", + WORKSPACE_PATH: env.get("BITBUCKET_CLONE_DIR"), + } + + +def extract_buildkite(env): + # type: (MutableMapping[str, str]) -> Dict[str, Optional[str]] + """Extract CI tags from Buildkite environ.""" + return { + git.BRANCH: env.get("BUILDKITE_BRANCH"), + git.COMMIT_SHA: env.get("BUILDKITE_COMMIT"), + git.REPOSITORY_URL: env.get("BUILDKITE_REPO"), + git.TAG: env.get("BUILDKITE_TAG"), + PIPELINE_ID: env.get("BUILDKITE_BUILD_ID"), + PIPELINE_NAME: env.get("BUILDKITE_PIPELINE_SLUG"), + PIPELINE_NUMBER: env.get("BUILDKITE_BUILD_NUMBER"), + PIPELINE_URL: env.get("BUILDKITE_BUILD_URL"), + JOB_URL: "{0}#{1}".format(env.get("BUILDKITE_BUILD_URL"), env.get("BUILDKITE_JOB_ID")), + PROVIDER_NAME: "buildkite", + WORKSPACE_PATH: env.get("BUILDKITE_BUILD_CHECKOUT_PATH"), + git.COMMIT_MESSAGE: env.get("BUILDKITE_MESSAGE"), + git.COMMIT_AUTHOR_NAME: env.get("BUILDKITE_BUILD_AUTHOR"), + git.COMMIT_AUTHOR_EMAIL: env.get("BUILDKITE_BUILD_AUTHOR_EMAIL"), + git.COMMIT_COMMITTER_NAME: env.get("BUILDKITE_BUILD_CREATOR"), + git.COMMIT_COMMITTER_EMAIL: env.get("BUILDKITE_BUILD_CREATOR_EMAIL"), + } + + +def extract_circle_ci(env): + # type: (MutableMapping[str, str]) -> Dict[str, Optional[str]] + """Extract CI tags from CircleCI environ.""" + return { + git.BRANCH: env.get("CIRCLE_BRANCH"), + git.COMMIT_SHA: env.get("CIRCLE_SHA1"), + git.REPOSITORY_URL: env.get("CIRCLE_REPOSITORY_URL"), + git.TAG: env.get("CIRCLE_TAG"), + PIPELINE_ID: env.get("CIRCLE_WORKFLOW_ID"), + PIPELINE_NAME: env.get("CIRCLE_PROJECT_REPONAME"), + PIPELINE_NUMBER: env.get("CIRCLE_BUILD_NUM"), + PIPELINE_URL: "https://app.circleci.com/pipelines/workflows/{0}".format(env.get("CIRCLE_WORKFLOW_ID")), + JOB_URL: env.get("CIRCLE_BUILD_URL"), + JOB_NAME: env.get("CIRCLE_JOB"), + PROVIDER_NAME: "circleci", + WORKSPACE_PATH: env.get("CIRCLE_WORKING_DIRECTORY"), + } + + +def extract_github_actions(env): + # type: (MutableMapping[str, str]) -> Dict[str, Optional[str]] + """Extract CI tags from Github environ.""" + branch_or_tag = env.get("GITHUB_HEAD_REF") or env.get("GITHUB_REF") or "" + branch = tag = None # type: Optional[str] + if "tags/" in branch_or_tag: + tag = branch_or_tag + else: + branch = branch_or_tag + return { + git.BRANCH: branch, + git.COMMIT_SHA: env.get("GITHUB_SHA"), + git.REPOSITORY_URL: "https://github.com/{0}.git".format(env.get("GITHUB_REPOSITORY")), + git.TAG: tag, + JOB_URL: "https://github.com/{0}/commit/{1}/checks".format(env.get("GITHUB_REPOSITORY"), env.get("GITHUB_SHA")), + PIPELINE_ID: env.get("GITHUB_RUN_ID"), + PIPELINE_NAME: env.get("GITHUB_WORKFLOW"), + PIPELINE_NUMBER: env.get("GITHUB_RUN_NUMBER"), + PIPELINE_URL: "https://github.com/{0}/commit/{1}/checks".format( + env.get("GITHUB_REPOSITORY"), env.get("GITHUB_SHA") + ), + PROVIDER_NAME: "github", + WORKSPACE_PATH: env.get("GITHUB_WORKSPACE"), + } + + +def extract_gitlab(env): + # type: (MutableMapping[str, str]) -> Dict[str, Optional[str]] + """Extract CI tags from Gitlab environ.""" + author = env.get("CI_COMMIT_AUTHOR") + author_name = None # type: Optional[str] + author_email = None # type: Optional[str] + if author: + # Extract name and email from `author` which is in the form "name " + author_name, author_email = author.strip("> ").split(" <") + commit_timestamp = env.get("CI_COMMIT_TIMESTAMP") + url = env.get("CI_PIPELINE_URL") + if url: + url = re.sub("/-/pipelines/", "/pipelines/", url) + return { + git.BRANCH: env.get("CI_COMMIT_REF_NAME"), + git.COMMIT_SHA: env.get("CI_COMMIT_SHA"), + git.REPOSITORY_URL: env.get("CI_REPOSITORY_URL"), + git.TAG: env.get("CI_COMMIT_TAG"), + STAGE_NAME: env.get("CI_JOB_STAGE"), + JOB_NAME: env.get("CI_JOB_NAME"), + JOB_URL: env.get("CI_JOB_URL"), + PIPELINE_ID: env.get("CI_PIPELINE_ID"), + PIPELINE_NAME: env.get("CI_PROJECT_PATH"), + PIPELINE_NUMBER: env.get("CI_PIPELINE_IID"), + PIPELINE_URL: url, + PROVIDER_NAME: "gitlab", + WORKSPACE_PATH: env.get("CI_PROJECT_DIR"), + git.COMMIT_MESSAGE: env.get("CI_COMMIT_MESSAGE"), + git.COMMIT_AUTHOR_NAME: author_name, + git.COMMIT_AUTHOR_EMAIL: author_email, + git.COMMIT_AUTHOR_DATE: commit_timestamp, + } + + +def extract_jenkins(env): + # type: (MutableMapping[str, str]) -> Dict[str, Optional[str]] + """Extract CI tags from Jenkins environ.""" + branch_or_tag = env.get("GIT_BRANCH", "") + branch = tag = None # type: Optional[str] + if "tags/" in branch_or_tag: + tag = branch_or_tag + else: + branch = branch_or_tag + name = env.get("JOB_NAME") + if name and branch: + name = re.sub("/{0}".format(_normalize_ref(branch)), "", name) + if name: + name = "/".join((v for v in name.split("/") if v and "=" not in v)) + + return { + git.BRANCH: branch, + git.COMMIT_SHA: env.get("GIT_COMMIT"), + git.REPOSITORY_URL: env.get("GIT_URL", env.get("GIT_URL_1")), + git.TAG: tag, + PIPELINE_ID: env.get("BUILD_TAG"), + PIPELINE_NAME: name, + PIPELINE_NUMBER: env.get("BUILD_NUMBER"), + PIPELINE_URL: env.get("BUILD_URL"), + PROVIDER_NAME: "jenkins", + WORKSPACE_PATH: env.get("WORKSPACE"), + } + + +def extract_teamcity(env): + # type: (MutableMapping[str, str]) -> Dict[str, Optional[str]] + """Extract CI tags from Teamcity environ.""" + return { + git.COMMIT_SHA: env.get("BUILD_VCS_NUMBER"), + git.REPOSITORY_URL: env.get("BUILD_VCS_URL"), + PIPELINE_ID: env.get("BUILD_ID"), + PIPELINE_NUMBER: env.get("BUILD_NUMBER"), + PIPELINE_URL: ( + "{0}/viewLog.html?buildId={1}".format(env.get("SERVER_URL"), env.get("BUILD_ID")) + if env.get("SERVER_URL") and env.get("BUILD_ID") + else None + ), + PROVIDER_NAME: "teamcity", + WORKSPACE_PATH: env.get("BUILD_CHECKOUTDIR"), + } + + +def extract_travis(env): + # type: (MutableMapping[str, str]) -> Dict[str, Optional[str]] + """Extract CI tags from Travis environ.""" + return { + git.BRANCH: env.get("TRAVIS_PULL_REQUEST_BRANCH") or env.get("TRAVIS_BRANCH"), + git.COMMIT_SHA: env.get("TRAVIS_COMMIT"), + git.REPOSITORY_URL: "https://github.com/{0}.git".format(env.get("TRAVIS_REPO_SLUG")), + git.TAG: env.get("TRAVIS_TAG"), + JOB_URL: env.get("TRAVIS_JOB_WEB_URL"), + PIPELINE_ID: env.get("TRAVIS_BUILD_ID"), + PIPELINE_NAME: env.get("TRAVIS_REPO_SLUG"), + PIPELINE_NUMBER: env.get("TRAVIS_BUILD_NUMBER"), + PIPELINE_URL: env.get("TRAVIS_BUILD_WEB_URL"), + PROVIDER_NAME: "travisci", + WORKSPACE_PATH: env.get("TRAVIS_BUILD_DIR"), + git.COMMIT_MESSAGE: env.get("TRAVIS_COMMIT_MESSAGE"), + } + + +def extract_bitrise(env): + # type: (MutableMapping[str, str]) -> Dict[str, Optional[str]] + """Extract CI tags from Bitrise environ.""" + commit = env.get("BITRISE_GIT_COMMIT") or env.get("GIT_CLONE_COMMIT_HASH") + branch = env.get("BITRISEIO_GIT_BRANCH_DEST") or env.get("BITRISE_GIT_BRANCH") + if env.get("BITRISE_GIT_MESSAGE"): + message = env.get("BITRISE_GIT_MESSAGE") # type: Optional[str] + elif env.get("GIT_CLONE_COMMIT_MESSAGE_SUBJECT") or env.get("GIT_CLONE_COMMIT_MESSAGE_BODY"): + message = "{0}:\n{1}".format( + env.get("GIT_CLONE_COMMIT_MESSAGE_SUBJECT"), env.get("GIT_CLONE_COMMIT_MESSAGE_BODY") + ) + else: + message = None + + return { + PROVIDER_NAME: "bitrise", + PIPELINE_ID: env.get("BITRISE_BUILD_SLUG"), + PIPELINE_NAME: env.get("BITRISE_TRIGGERED_WORKFLOW_ID"), + PIPELINE_NUMBER: env.get("BITRISE_BUILD_NUMBER"), + PIPELINE_URL: env.get("BITRISE_BUILD_URL"), + WORKSPACE_PATH: env.get("BITRISE_SOURCE_DIR"), + git.REPOSITORY_URL: env.get("GIT_REPOSITORY_URL"), + git.COMMIT_SHA: commit, + git.BRANCH: branch, + git.TAG: env.get("BITRISE_GIT_TAG"), + git.COMMIT_MESSAGE: message, + git.COMMIT_AUTHOR_NAME: env.get("GIT_CLONE_COMMIT_AUTHOR_NAME"), + git.COMMIT_AUTHOR_EMAIL: env.get("GIT_CLONE_COMMIT_AUTHOR_EMAIL"), + git.COMMIT_COMMITTER_NAME: env.get("GIT_CLONE_COMMIT_COMMITER_NAME"), + git.COMMIT_COMMITTER_EMAIL: env.get("GIT_CLONE_COMMIT_COMMITER_NAME"), + } + + +PROVIDERS = ( + ("APPVEYOR", extract_appveyor), + ("TF_BUILD", extract_azure_pipelines), + ("BITBUCKET_COMMIT", extract_bitbucket), + ("BUILDKITE", extract_buildkite), + ("CIRCLECI", extract_circle_ci), + ("GITHUB_SHA", extract_github_actions), + ("GITLAB_CI", extract_gitlab), + ("JENKINS_URL", extract_jenkins), + ("TEAMCITY_VERSION", extract_teamcity), + ("TRAVIS", extract_travis), + ("BITRISE_BUILD_SLUG", extract_bitrise), +) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/consul.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/consul.py new file mode 100644 index 000000000..72b9986da --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/consul.py @@ -0,0 +1,4 @@ +APP = "consul" +SERVICE = "consul" +CMD = "consul.command" +KEY = "consul.key" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/db.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/db.py new file mode 100644 index 000000000..8050590cc --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/db.py @@ -0,0 +1,4 @@ +# tags +NAME = "db.name" # the database name (eg: dbname for pgsql) +USER = "db.user" # the user connecting to the db +ROWCOUNT = "db.rowcount" # the rowcount of a query diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/elasticsearch.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/elasticsearch.py new file mode 100644 index 000000000..ca42c0be7 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/elasticsearch.py @@ -0,0 +1,9 @@ +SERVICE = "elasticsearch" +APP = "elasticsearch" + +# standard tags +URL = "elasticsearch.url" +METHOD = "elasticsearch.method" +TOOK = "elasticsearch.took" +PARAMS = "elasticsearch.params" +BODY = "elasticsearch.body" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/errors.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/errors.py new file mode 100644 index 000000000..9ec4ed3c8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/errors.py @@ -0,0 +1,37 @@ +""" +tags for common error attributes +""" + +import traceback + +from ddtrace.constants import ERROR_MSG +from ddtrace.constants import ERROR_STACK +from ddtrace.constants import ERROR_TYPE +from ddtrace.utils.deprecation import deprecated +from ddtrace.utils.deprecation import deprecation + + +__all__ = [ERROR_MSG, ERROR_TYPE, ERROR_STACK] + +deprecation( + name="ddtrace.ext.errors", + message=( + "Use `ddtrace.constants` module instead. " + "Shorthand error constants will be removed. Use ERROR_MSG, ERROR_TYPE, and ERROR_STACK instead." + ), + version="1.0.0", +) + +# shorthand for ERROR constants to be removed in v1.0-----^ +MSG = ERROR_MSG +TYPE = ERROR_TYPE +STACK = ERROR_STACK + + +@deprecated("This method and module will be removed altogether", "1.0.0") +def get_traceback(tb=None, error=None): + t = None + if error: + t = type(error) + lines = traceback.format_exception(t, error, tb, limit=20) + return "\n".join(lines) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/git.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/git.py new file mode 100644 index 000000000..5e96ded1b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/git.py @@ -0,0 +1,161 @@ +""" +tags for common git attributes +""" +import os +import subprocess +from typing import Dict +from typing import MutableMapping +from typing import Optional +from typing import Tuple + +import six + +from ddtrace.internal import compat +from ddtrace.internal.logger import get_logger + + +if six.PY2: + GitNotFoundError = OSError +else: + GitNotFoundError = FileNotFoundError + +# Git Branch +BRANCH = "git.branch" + +# Git Commit SHA +COMMIT_SHA = "git.commit.sha" + +# Git Repository URL +REPOSITORY_URL = "git.repository_url" + +# Git Tag +TAG = "git.tag" + +# Git Commit Author Name +COMMIT_AUTHOR_NAME = "git.commit.author.name" + +# Git Commit Author Email +COMMIT_AUTHOR_EMAIL = "git.commit.author.email" + +# Git Commit Author Date (UTC) +COMMIT_AUTHOR_DATE = "git.commit.author.date" + +# Git Commit Committer Name +COMMIT_COMMITTER_NAME = "git.commit.committer.name" + +# Git Commit Committer Email +COMMIT_COMMITTER_EMAIL = "git.commit.committer.email" + +# Git Commit Committer Date (UTC) +COMMIT_COMMITTER_DATE = "git.commit.committer.date" + +# Git Commit Message +COMMIT_MESSAGE = "git.commit.message" + +log = get_logger(__name__) + + +def _git_subprocess_cmd(cmd, cwd=None): + # type: (str, Optional[str]) -> str + """Helper for invoking the git CLI binary.""" + git_cmd = cmd.split(" ") + git_cmd.insert(0, "git") + process = subprocess.Popen(git_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd) + stdout, stderr = process.communicate() + if process.returncode == 0: + return compat.ensure_text(stdout).strip() + raise ValueError(stderr) + + +def extract_user_info(cwd=None): + # type: (Optional[str]) -> Dict[str, Tuple[str, str, str]] + """Extract commit author info from the git repository in the current directory or one specified by ``cwd``.""" + # Note: `git show -s --format... --date...` is supported since git 2.1.4 onwards + stdout = _git_subprocess_cmd("show -s --format=%an,%ae,%ad,%cn,%ce,%cd --date=format:%Y-%m-%dT%H:%M:%S%z", cwd=cwd) + author_name, author_email, author_date, committer_name, committer_email, committer_date = stdout.split(",") + return { + "author": (author_name, author_email, author_date), + "committer": (committer_name, committer_email, committer_date), + } + + +def extract_repository_url(cwd=None): + # type: (Optional[str]) -> str + """Extract the repository url from the git repository in the current directory or one specified by ``cwd``.""" + # Note: `git show ls-remote --get-url` is supported since git 2.6.7 onwards + repository_url = _git_subprocess_cmd("ls-remote --get-url", cwd=cwd) + return repository_url + + +def extract_commit_message(cwd=None): + # type: (Optional[str]) -> str + """Extract git commit message from the git repository in the current directory or one specified by ``cwd``.""" + # Note: `git show -s --format... --date...` is supported since git 2.1.4 onwards + commit_message = _git_subprocess_cmd("show -s --format=%s", cwd=cwd) + return commit_message + + +def extract_workspace_path(cwd=None): + # type: (Optional[str]) -> str + """Extract the root directory path from the git repository in the current directory or one specified by ``cwd``.""" + workspace_path = _git_subprocess_cmd("rev-parse --show-toplevel", cwd=cwd) + return workspace_path + + +def extract_branch(cwd=None): + # type: (Optional[str]) -> str + """Extract git branch from the git repository in the current directory or one specified by ``cwd``.""" + branch = _git_subprocess_cmd("rev-parse --abbrev-ref HEAD", cwd=cwd) + return branch + + +def extract_commit_sha(cwd=None): + # type: (Optional[str]) -> str + """Extract git commit SHA from the git repository in the current directory or one specified by ``cwd``.""" + commit_sha = _git_subprocess_cmd("rev-parse HEAD", cwd=cwd) + return commit_sha + + +def extract_git_metadata(cwd=None): + # type: (Optional[str]) -> Dict[str, Optional[str]] + """Extract git commit metadata.""" + tags = {} # type: Dict[str, Optional[str]] + try: + tags[REPOSITORY_URL] = extract_repository_url(cwd=cwd) + tags[COMMIT_MESSAGE] = extract_commit_message(cwd=cwd) + users = extract_user_info(cwd=cwd) + tags[COMMIT_AUTHOR_NAME] = users["author"][0] + tags[COMMIT_AUTHOR_EMAIL] = users["author"][1] + tags[COMMIT_AUTHOR_DATE] = users["author"][2] + tags[COMMIT_COMMITTER_NAME] = users["committer"][0] + tags[COMMIT_COMMITTER_EMAIL] = users["committer"][1] + tags[COMMIT_COMMITTER_DATE] = users["committer"][2] + tags[BRANCH] = extract_branch(cwd=cwd) + tags[COMMIT_SHA] = extract_commit_sha(cwd=cwd) + except GitNotFoundError: + log.error("Git executable not found, cannot extract git metadata.") + except ValueError: + log.error("Error extracting git metadata, received non-zero return code.", exc_info=True) + + return tags + + +def extract_user_git_metadata(env=None): + # type: (Optional[MutableMapping[str, str]]) -> Dict[str, Optional[str]] + """Extract git commit metadata from user-provided env vars.""" + env = os.environ if env is None else env + + tags = {} + tags[REPOSITORY_URL] = env.get("DD_GIT_REPOSITORY_URL") + tags[COMMIT_SHA] = env.get("DD_GIT_COMMIT_SHA") + tags[BRANCH] = env.get("DD_GIT_BRANCH") + tags[TAG] = env.get("DD_GIT_TAG") + tags[COMMIT_MESSAGE] = env.get("DD_GIT_COMMIT_MESSAGE") + tags[COMMIT_AUTHOR_DATE] = env.get("DD_GIT_COMMIT_AUTHOR_DATE") + tags[COMMIT_AUTHOR_EMAIL] = env.get("DD_GIT_COMMIT_AUTHOR_EMAIL") + tags[COMMIT_AUTHOR_NAME] = env.get("DD_GIT_COMMIT_AUTHOR_NAME") + tags[COMMIT_COMMITTER_DATE] = env.get("DD_GIT_COMMIT_COMMITTER_DATE") + tags[COMMIT_COMMITTER_EMAIL] = env.get("DD_GIT_COMMIT_COMMITTER_EMAIL") + tags[COMMIT_COMMITTER_NAME] = env.get("DD_GIT_COMMIT_COMMITTER_NAME") + + return tags diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/http.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/http.py new file mode 100644 index 000000000..86f6ea02f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/http.py @@ -0,0 +1,19 @@ +""" +Standard http tags. + +For example: + +span.set_tag(URL, '/user/home') +span.set_tag(STATUS_CODE, 404) +""" +# tags +URL = "http.url" +METHOD = "http.method" +STATUS_CODE = "http.status_code" +STATUS_MSG = "http.status_msg" +QUERY_STRING = "http.query.string" +RETRIES_REMAIN = "http.retries_remain" +VERSION = "http.version" + +# template render span type +TEMPLATE = "template" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/kombu.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/kombu.py new file mode 100644 index 000000000..44a25f561 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/kombu.py @@ -0,0 +1,12 @@ +SERVICE = "kombu" + +# net extension +VHOST = "out.vhost" + +# standard tags +EXCHANGE = "kombu.exchange" +BODY_LEN = "kombu.body_length" +ROUTING_KEY = "kombu.routing_key" + +PUBLISH_NAME = "kombu.publish" +RECEIVE_NAME = "kombu.receive" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/memcached.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/memcached.py new file mode 100644 index 000000000..349716d54 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/memcached.py @@ -0,0 +1,3 @@ +CMD = "memcached.command" +SERVICE = "memcached" +QUERY = "memcached.query" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/mongo.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/mongo.py new file mode 100644 index 000000000..86e8e18fd --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/mongo.py @@ -0,0 +1,5 @@ +SERVICE = "mongodb" +COLLECTION = "mongodb.collection" +DB = "mongodb.db" +ROWS = "mongodb.rows" +QUERY = "mongodb.query" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/net.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/net.py new file mode 100644 index 000000000..b054fdfab --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/net.py @@ -0,0 +1,9 @@ +""" +Standard network tags. +""" + +# request targets +TARGET_HOST = "out.host" +TARGET_PORT = "out.port" + +BYTES_OUT = "net.out.bytes" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/priority.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/priority.py new file mode 100644 index 000000000..8a51480b5 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/priority.py @@ -0,0 +1,29 @@ +""" +Priority is a hint given to the backend so that it knows which traces to reject or kept. +In a distributed context, it should be set before any context propagation (fork, RPC calls) to be effective. + +For example: + +from ddtrace.ext.priority import USER_REJECT, USER_KEEP + +context = tracer.context_provider.active() +# Indicate to not keep the trace +context.sampling_priority = USER_REJECT + +# Indicate to keep the trace +span.context.sampling_priority = USER_KEEP +""" +from ddtrace.constants import AUTO_KEEP +from ddtrace.constants import AUTO_REJECT +from ddtrace.constants import USER_KEEP +from ddtrace.constants import USER_REJECT +from ddtrace.utils.deprecation import deprecation + + +deprecation( + name="ddtrace.ext.priority", + message="Use `ddtrace.constants` module instead", + version="1.0.0", +) + +__all__ = ["USER_REJECT", "AUTO_REJECT", "AUTO_KEEP", "USER_KEEP"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/redis.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/redis.py new file mode 100644 index 000000000..3233623c8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/redis.py @@ -0,0 +1,13 @@ +# defaults +APP = "redis" +DEFAULT_SERVICE = "redis" + +# net extension +DB = "out.redis_db" + +# standard tags +RAWCMD = "redis.raw_command" +CMD = "redis.command" +ARGS_LEN = "redis.args_length" +PIPELINE_LEN = "redis.pipeline_length" +PIPELINE_AGE = "redis.pipeline_age" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/sql.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/sql.py new file mode 100644 index 000000000..e4208f28d --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/sql.py @@ -0,0 +1,34 @@ +from typing import Dict + + +# tags +QUERY = "sql.query" # the query text +ROWS = "sql.rows" # number of rows returned by a query +DB = "sql.db" # the name of the database + + +def normalize_vendor(vendor): + # type: (str) -> str + """Return a canonical name for a type of database.""" + if not vendor: + return "db" # should this ever happen? + elif "sqlite" in vendor: + return "sqlite" + elif "postgres" in vendor or vendor == "psycopg2": + return "postgres" + else: + return vendor + + +try: + from psycopg2.extensions import parse_dsn as parse_pg_dsn +except ImportError: + + def parse_pg_dsn(dsn): + # type: (str) -> Dict[str, str] + """ + Return a dictionary of the components of a postgres DSN. + >>> parse_pg_dsn('user=dog port=1543 dbname=dogdata') + {'user':'dog', 'port':'1543', 'dbname':'dogdata'} + """ + return dict(_.split("=", maxsplit=1) for _ in dsn.split()) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/system.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/system.py new file mode 100644 index 000000000..8e75bd938 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/system.py @@ -0,0 +1,14 @@ +""" +Standard system tags +""" +from ddtrace.constants import PID +from ddtrace.utils.deprecation import deprecation + + +deprecation( + name="ddtrace.ext.system", + message="Use `ddtrace.constants` module instead", + version="1.0.0", +) + +__all__ = ["PID"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/test.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/test.py new file mode 100644 index 000000000..567c36680 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/ext/test.py @@ -0,0 +1,50 @@ +""" +tags for common test attributes +""" + +from enum import Enum + + +# Test Arguments +ARGUMENTS = TEST_ARGUMENTS = "test.arguments" + +# Test Framework +FRAMEWORK = TEST_FRAMEWORK = "test.framework" + +# Test Framework Version +FRAMEWORK_VERSION = TEST_FRAMEWORK_VERSION = "test.framework_version" + +# Test Name +NAME = TEST_NAME = "test.name" + +# Test Parameters +PARAMETERS = "test.parameters" + +# Pytest Result (XFail, XPass) +RESULT = TEST_RESULT = "pytest.result" + +# Skip Reason +SKIP_REASON = TEST_SKIP_REASON = "test.skip_reason" + +# Test Status +STATUS = TEST_STATUS = "test.status" + +# Test Suite +SUITE = TEST_SUITE = "test.suite" + +# Traits +TRAITS = TEST_TRAITS = "test.traits" + +# Test Type +TYPE = TEST_TYPE = "test.type" + +# XFail Reason +XFAIL_REASON = TEST_XFAIL_REASON = "pytest.xfail.reason" + + +class Status(Enum): + PASS = "pass" + FAIL = "fail" + SKIP = "skip" + XFAIL = "xfail" + XPASS = "xpass" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/filters.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/filters.py new file mode 100644 index 000000000..6725be960 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/filters.py @@ -0,0 +1,72 @@ +import abc +import re +from typing import List +from typing import Optional +from typing import TYPE_CHECKING + +from ddtrace.internal.processor.trace import TraceProcessor + +from .ext import http + + +if TYPE_CHECKING: + from ddtrace import Span + + +class TraceFilter(TraceProcessor): + @abc.abstractmethod + def process_trace(self, trace): + # type: (List[Span]) -> Optional[List[Span]] + """Processes a trace. + + None can be returned to prevent the trace from being exported. + """ + pass + + +class FilterRequestsOnUrl(TraceFilter): + r"""Filter out traces from incoming http requests based on the request's url. + + This class takes as argument a list of regular expression patterns + representing the urls to be excluded from tracing. A trace will be excluded + if its root span contains a ``http.url`` tag and if this tag matches any of + the provided regular expression using the standard python regexp match + semantic (https://docs.python.org/2/library/re.html#re.match). + + :param list regexps: a list of regular expressions (or a single string) defining + the urls that should be filtered out. + + Examples: + To filter out http calls to domain api.example.com:: + + FilterRequestsOnUrl(r'http://api\\.example\\.com') + + To filter out http calls to all first level subdomains from example.com:: + + FilterRequestOnUrl(r'http://.*+\\.example\\.com') + + To filter out calls to both http://test.example.com and http://example.com/healthcheck:: + + FilterRequestOnUrl([r'http://test\\.example\\.com', r'http://example\\.com/healthcheck']) + """ + + def __init__(self, regexps): + if isinstance(regexps, str): + regexps = [regexps] + self._regexps = [re.compile(regexp) for regexp in regexps] + + def process_trace(self, trace): + # type: (List[Span]) -> Optional[List[Span]] + """ + When the filter is registered in the tracer, process_trace is called by + on each trace before it is sent to the agent, the returned value will + be fed to the next filter in the list. If process_trace returns None, + the whole trace is discarded. + """ + for span in trace: + if span.parent_id is None and span.get_tag(http.URL) is not None: + url = span.get_tag(http.URL) + for regexp in self._regexps: + if regexp.match(url): + return None + return trace diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/helpers.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/helpers.py new file mode 100644 index 000000000..133db85dc --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/helpers.py @@ -0,0 +1,48 @@ +from typing import Optional +from typing import TYPE_CHECKING +from typing import Tuple + +import ddtrace +from ddtrace.utils.deprecation import deprecated + + +if TYPE_CHECKING: + from ddtrace.tracer import Tracer + + +@deprecated("This method and module will be removed altogether", "1.0.0") +def get_correlation_ids(tracer=None): + # type: (Optional[Tracer]) -> Tuple[Optional[int], Optional[int]] + """Retrieves the Correlation Identifiers for the current active ``Trace``. + This helper method can be achieved manually and should be considered + only a shortcut. The main reason is to abstract the current ``Tracer`` + implementation so that these identifiers can be extracted either the + tracer is an OpenTracing tracer or a Datadog tracer. + + OpenTracing users can still extract these values using the ``ScopeManager`` + API, though this shortcut is a simple one-liner. The usage is: + + from ddtrace import helpers + + trace_id, span_id = helpers.get_correlation_ids() + + :returns: a tuple containing the trace_id and span_id + """ + # Consideration: currently we don't have another way to "define" a + # GlobalTracer. In the case of OpenTracing, ``opentracing.tracer`` is exposed + # and we're doing the same here for ``ddtrace.tracer``. Because this helper + # must work also with OpenTracing, we should take the right used ``Tracer``. + # At the time of writing, it's enough to support our Datadog Tracer. + + # If no tracer passed in, use global tracer + if not tracer: + tracer = ddtrace.tracer + + # If tracer is disabled, skip + if not tracer.enabled: + return None, None + + span = tracer.current_span() + if not span: + return None, None + return span.trace_id, span.span_id diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/http/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/http/__init__.py new file mode 100644 index 000000000..8e3442a3b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/http/__init__.py @@ -0,0 +1,16 @@ +from ddtrace.utils.deprecation import deprecation + +from .headers import store_request_headers +from .headers import store_response_headers + + +__all__ = [ + "store_request_headers", + "store_response_headers", +] + +deprecation( + name="ddtrace.http", + message="The http module has been merged into ddtrace.contrib.trace_utils", + version="1.0.0", +) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/http/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/http/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..2ce514c90 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/http/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/http/__pycache__/headers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/http/__pycache__/headers.cpython-38.pyc new file mode 100644 index 000000000..64921ea8f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/http/__pycache__/headers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/http/headers.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/http/headers.py new file mode 100644 index 000000000..c48e672a6 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/http/headers.py @@ -0,0 +1,27 @@ +from ddtrace.contrib.trace_utils import NORMALIZE_PATTERN +from ddtrace.contrib.trace_utils import REQUEST +from ddtrace.contrib.trace_utils import RESPONSE +from ddtrace.contrib.trace_utils import _normalize_tag_name +from ddtrace.contrib.trace_utils import _normalized_header_name +from ddtrace.contrib.trace_utils import _store_headers +from ddtrace.contrib.trace_utils import _store_request_headers as store_request_headers +from ddtrace.contrib.trace_utils import _store_response_headers as store_response_headers +from ddtrace.utils.deprecation import deprecation + + +__all__ = ( + "store_request_headers", + "store_response_headers", + "NORMALIZE_PATTERN", + "REQUEST", + "RESPONSE", + "_store_headers", + "_normalized_header_name", + "_normalize_tag_name", +) + +deprecation( + name="ddtrace.http.headers", + message="The http.headers module has been merged into ddtrace.contrib.trace_utils", + version="1.0.0", +) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..b7293900b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/agent.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/agent.cpython-38.pyc new file mode 100644 index 000000000..1edb6401e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/agent.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/atexit.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/atexit.cpython-38.pyc new file mode 100644 index 000000000..ae0accf66 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/atexit.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/compat.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/compat.cpython-38.pyc new file mode 100644 index 000000000..0bd3a7945 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/compat.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/debug.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/debug.cpython-38.pyc new file mode 100644 index 000000000..3d52f2a7b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/debug.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/dogstatsd.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/dogstatsd.cpython-38.pyc new file mode 100644 index 000000000..68d9e7aa9 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/dogstatsd.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/encoding.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/encoding.cpython-38.pyc new file mode 100644 index 000000000..100c3b6e0 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/encoding.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/forksafe.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/forksafe.cpython-38.pyc new file mode 100644 index 000000000..902c4a84f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/forksafe.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/hostname.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/hostname.cpython-38.pyc new file mode 100644 index 000000000..1aa5fab36 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/hostname.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/http.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/http.cpython-38.pyc new file mode 100644 index 000000000..055ea7d54 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/http.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/logger.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/logger.cpython-38.pyc new file mode 100644 index 000000000..5c079a365 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/logger.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/nogevent.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/nogevent.cpython-38.pyc new file mode 100644 index 000000000..c977e518b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/nogevent.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/periodic.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/periodic.cpython-38.pyc new file mode 100644 index 000000000..8edb519a4 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/periodic.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/rate_limiter.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/rate_limiter.cpython-38.pyc new file mode 100644 index 000000000..c949c2dcd Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/rate_limiter.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/service.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/service.cpython-38.pyc new file mode 100644 index 000000000..393559087 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/service.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/sma.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/sma.cpython-38.pyc new file mode 100644 index 000000000..f2b212a49 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/sma.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/uds.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/uds.cpython-38.pyc new file mode 100644 index 000000000..927fb1817 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/uds.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/uwsgi.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/uwsgi.cpython-38.pyc new file mode 100644 index 000000000..036242925 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/uwsgi.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/writer.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/writer.cpython-38.pyc new file mode 100644 index 000000000..73c698c59 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/__pycache__/writer.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/_encoding.cpython-38-darwin.so b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/_encoding.cpython-38-darwin.so new file mode 100755 index 000000000..e23047dfe Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/_encoding.cpython-38-darwin.so differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/_rand.cpython-38-darwin.so b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/_rand.cpython-38-darwin.so new file mode 100755 index 000000000..96ef7a859 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/_rand.cpython-38-darwin.so differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/agent.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/agent.py new file mode 100644 index 000000000..fbcda2015 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/agent.py @@ -0,0 +1,130 @@ +import os +from typing import TypeVar +from typing import Union + +from ddtrace.internal.compat import parse +from ddtrace.utils.formats import get_env + +from .http import HTTPConnection +from .http import HTTPSConnection +from .uds import UDSHTTPConnection + + +DEFAULT_HOSTNAME = "localhost" +DEFAULT_TRACE_PORT = 8126 +DEFAULT_UNIX_TRACE_PATH = "/var/run/datadog/apm.socket" +DEFAULT_UNIX_DSD_PATH = "/var/run/datadog/dsd.socket" +DEFAULT_STATS_PORT = 8125 +DEFAULT_TRACE_URL = "http://%s:%s" % (DEFAULT_HOSTNAME, DEFAULT_TRACE_PORT) +DEFAULT_TIMEOUT = 2.0 + +ConnectionType = Union[HTTPSConnection, HTTPConnection, UDSHTTPConnection] + +T = TypeVar("T") + + +def get_trace_hostname(default=DEFAULT_HOSTNAME): + # type: (Union[T, str]) -> Union[T, str] + return os.environ.get("DD_AGENT_HOST", os.environ.get("DATADOG_TRACE_AGENT_HOSTNAME", default)) + + +def get_stats_hostname(default=DEFAULT_HOSTNAME): + # type: (Union[T, str]) -> Union[T, str] + return os.environ.get("DD_AGENT_HOST", os.environ.get("DD_DOGSTATSD_HOST", default)) + + +def get_trace_port(default=DEFAULT_TRACE_PORT): + # type: (Union[T, int]) -> Union[T,int] + v = os.environ.get("DD_AGENT_PORT", os.environ.get("DD_TRACE_AGENT_PORT")) + if v is not None: + return int(v) + return default + + +def get_stats_port(default=DEFAULT_STATS_PORT): + # type: (Union[T, int]) -> Union[T,int] + v = get_env("dogstatsd", "port", default=None) + if v is not None: + return int(v) + return default + + +def get_trace_agent_timeout(): + # type: () -> float + return float(get_env("trace", "agent", "timeout", "seconds", default=DEFAULT_TIMEOUT)) # type: ignore[arg-type] + + +def get_trace_url(): + # type: () -> str + """Return the Agent URL computed from the environment. + + Raises a ``ValueError`` if the URL is not supported by the Agent. + """ + user_supplied_host = get_trace_hostname(None) is not None + user_supplied_port = get_trace_port(None) is not None + + url = os.environ.get("DD_TRACE_AGENT_URL") + + if not url: + if user_supplied_host or user_supplied_port: + url = "http://%s:%s" % (get_trace_hostname(), get_trace_port()) + elif os.path.exists("/var/run/datadog/apm.socket"): + url = "unix://%s" % (DEFAULT_UNIX_TRACE_PATH) + else: + url = DEFAULT_TRACE_URL + + return url + + +def get_stats_url(): + # type: () -> str + user_supplied_host = get_stats_hostname(None) is not None + user_supplied_port = get_stats_port(None) is not None + + url = get_env("dogstatsd", "url", default=None) + + if not url: + if user_supplied_host or user_supplied_port: + url = "udp://{}:{}".format(get_stats_hostname(), get_stats_port()) + elif os.path.exists("/var/run/datadog/dsd.socket"): + url = "unix://%s" % (DEFAULT_UNIX_DSD_PATH) + else: + url = "udp://{}:{}".format(get_stats_hostname(), get_stats_port()) + return url + + +def verify_url(url): + # type: (str) -> parse.ParseResult + """Verify that a URL can be used to communicate with the Datadog Agent. + Returns a parse.ParseResult. + Raises a ``ValueError`` if the URL is not supported by the Agent. + """ + parsed = parse.urlparse(url) + schemes = ("http", "https", "unix") + if parsed.scheme not in schemes: + raise ValueError( + "Unsupported protocol '%s' in Agent URL '%s'. Must be one of: %s" % (parsed.scheme, url, ", ".join(schemes)) + ) + elif parsed.scheme in ["http", "https"] and not parsed.hostname: + raise ValueError("Invalid hostname in Agent URL '%s'" % url) + elif parsed.scheme == "unix" and not parsed.path: + raise ValueError("Invalid file path in Agent URL '%s'" % url) + + return parsed + + +def get_connection(url, timeout=DEFAULT_TIMEOUT): + # type: (str, float) -> ConnectionType + """Return an HTTP connection to the given URL.""" + parsed = verify_url(url) + hostname = parsed.hostname or "" + path = parsed.path or "/" + + if parsed.scheme == "https": + return HTTPSConnection.with_base_path(hostname, parsed.port, base_path=path, timeout=timeout) + elif parsed.scheme == "http": + return HTTPConnection.with_base_path(hostname, parsed.port, base_path=path, timeout=timeout) + elif parsed.scheme == "unix": + return UDSHTTPConnection(path, hostname, parsed.port, timeout=timeout) + + raise ValueError("Unsupported protocol '%s'" % parsed.scheme) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/atexit.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/atexit.py new file mode 100644 index 000000000..d7d308cdd --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/atexit.py @@ -0,0 +1,58 @@ +# -*- encoding: utf-8 -*- +""" +An API to provide atexit functionalities +""" +from __future__ import absolute_import + +import atexit +import logging +import typing + + +log = logging.getLogger(__name__) + + +if hasattr(atexit, "unregister"): + register = atexit.register + unregister = atexit.unregister +else: + # Hello Python 2! + _registry = [] # type: typing.List[typing.Tuple[typing.Callable[..., None], typing.Tuple, typing.Dict]] + + def _ddtrace_atexit(): + # type: (...) -> None + """Wrapper function that calls all registered function on normal program termination""" + global _registry + + # DEV: we make a copy of the registry to prevent hook execution from + # introducing new hooks, potentially causing an infinite loop. + for hook, args, kwargs in list(_registry): + try: + hook(*args, **kwargs) + except Exception: + # Mimic the behaviour of Python's atexit hooks. + log.exception("Error in atexit hook %r", hook) + + def register( + func, # type: typing.Callable[..., typing.Any] + *args, # type: typing.Any + **kwargs # type: typing.Any + ): + # type: (...) -> typing.Callable[..., typing.Any] + """Register a function to be executed upon normal program termination""" + + _registry.append((func, args, kwargs)) + return func + + def unregister(func): + # type: (typing.Callable[..., None]) -> None + """ + Unregister an exit function which was previously registered using + atexit.register. + If the function was not registered, it is ignored. + """ + global _registry + + _registry = [(f, args, kwargs) for f, args, kwargs in _registry if f != func] + + atexit.register(_ddtrace_atexit) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/compat.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/compat.py new file mode 100644 index 000000000..250d97bff --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/compat.py @@ -0,0 +1,253 @@ +import platform +import random +import re +import sys +import textwrap +import threading +from typing import Any +from typing import AnyStr +from typing import Optional +from typing import Text +from typing import Union + +import six + + +__all__ = [ + "httplib", + "iteritems", + "PY2", + "Queue", + "stringify", + "StringIO", + "urlencode", + "parse", + "reraise", + "maybe_stringify", +] + +PYTHON_VERSION_INFO = sys.version_info +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + +# Infos about python passed to the trace agent through the header +PYTHON_VERSION = platform.python_version() +PYTHON_INTERPRETER = platform.python_implementation() + +try: + StringIO = six.moves.cStringIO +except ImportError: + StringIO = six.StringIO # type: ignore[misc] + +httplib = six.moves.http_client +urlencode = six.moves.urllib.parse.urlencode +parse = six.moves.urllib.parse +Queue = six.moves.queue.Queue +iteritems = six.iteritems +reraise = six.reraise +reload_module = six.moves.reload_module + +ensure_text = six.ensure_text +stringify = six.text_type +string_type = six.string_types[0] +binary_type = six.binary_type +msgpack_type = six.binary_type +# DEV: `six` doesn't have `float` in `integer_types` +numeric_types = six.integer_types + (float,) + +# `six.integer_types` cannot be used for typing as we need to define a type +# alias for +# see https://mypy.readthedocs.io/en/latest/common_issues.html#variables-vs-type-aliases +if PY3: + NumericType = Union[int, float] +else: + NumericType = Union[long, int, float] # noqa: F821 + +# Pattern class generated by `re.compile` +if PYTHON_VERSION_INFO >= (3, 7): + pattern_type = re.Pattern +else: + pattern_type = re._pattern_type # type: ignore[misc,attr-defined] + + +def is_integer(obj): + # type: (Any) -> bool + """Helper to determine if the provided ``obj`` is an integer type or not""" + # DEV: We have to make sure it is an integer and not a boolean + # >>> type(True) + # + # >>> isinstance(True, int) + # True + return isinstance(obj, six.integer_types) and not isinstance(obj, bool) + + +try: + from time import time_ns +except ImportError: + from time import time as _time + + def time_ns(): + # type: () -> int + return int(_time() * 10e5) * 1000 + + +try: + from time import monotonic +except ImportError: + from ddtrace.vendor.monotonic import monotonic + + +try: + from time import monotonic_ns +except ImportError: + + def monotonic_ns(): + # type: () -> int + return int(monotonic() * 1e9) + + +try: + from time import process_time_ns +except ImportError: + from time import clock as _process_time # type: ignore[attr-defined] + + def process_time_ns(): + # type: () -> int + return int(_process_time() * 1e9) + + +if sys.version_info.major < 3: + getrandbits = random.SystemRandom().getrandbits +else: + getrandbits = random.getrandbits + + +if sys.version_info.major < 3: + if isinstance(threading.current_thread(), threading._MainThread): # type: ignore[attr-defined] + main_thread = threading.current_thread() + else: + main_thread = threading._shutdown.im_self # type: ignore[attr-defined] +else: + main_thread = threading.main_thread() + + +if PYTHON_VERSION_INFO[0:2] >= (3, 4): + from asyncio import iscoroutinefunction + + # Execute from a string to get around syntax errors from `yield from` + # DEV: The idea to do this was stolen from `six` + # https://github.com/benjaminp/six/blob/15e31431af97e5e64b80af0a3f598d382bcdd49a/six.py#L719-L737 + six.exec_( + textwrap.dedent( + """ + import functools + import asyncio + + + def make_async_decorator(tracer, coro, *params, **kw_params): + \"\"\" + Decorator factory that creates an asynchronous wrapper that yields + a coroutine result. This factory is required to handle Python 2 + compatibilities. + + :param object tracer: the tracer instance that is used + :param function f: the coroutine that must be executed + :param tuple params: arguments given to the Tracer.trace() + :param dict kw_params: keyword arguments given to the Tracer.trace() + \"\"\" + @functools.wraps(coro) + @asyncio.coroutine + def func_wrapper(*args, **kwargs): + with tracer.trace(*params, **kw_params): + result = yield from coro(*args, **kwargs) # noqa: E999 + return result + + return func_wrapper + """ + ) + ) + +else: + # asyncio is missing so we can't have coroutines; these + # functions are used only to ensure code executions in case + # of an unexpected behavior + def iscoroutinefunction(fn): # type: ignore + return False + + def make_async_decorator(tracer, fn, *params, **kw_params): + return fn + + +# DEV: There is `six.u()` which does something similar, but doesn't have the guard around `hasattr(s, 'decode')` +def to_unicode(s): + # type: (AnyStr) -> Text + """Return a unicode string for the given bytes or string instance.""" + # No reason to decode if we already have the unicode compatible object we expect + # DEV: `six.text_type` will be a `str` for python 3 and `unicode` for python 2 + # DEV: Double decoding a `unicode` can cause a `UnicodeEncodeError` + # e.g. `'\xc3\xbf'.decode('utf-8').decode('utf-8')` + if isinstance(s, six.text_type): + return s + + # If the object has a `decode` method, then decode into `utf-8` + # e.g. Python 2 `str`, Python 2/3 `bytearray`, etc + if hasattr(s, "decode"): + return s.decode("utf-8") + + # Always try to coerce the object into the `six.text_type` object we expect + # e.g. `to_unicode(1)`, `to_unicode(dict(key='value'))` + return six.text_type(s) + + +def get_connection_response( + conn, # type: httplib.HTTPConnection +): + # type: (...) -> httplib.HTTPResponse + """Returns the response for a connection. + + If using Python 2 enable buffering. + + Python 2 does not enable buffering by default resulting in many recv + syscalls. + + See: + https://bugs.python.org/issue4879 + https://github.com/python/cpython/commit/3c43fcba8b67ea0cec4a443c755ce5f25990a6cf + """ + if PY2: + return conn.getresponse(buffering=True) + else: + return conn.getresponse() + + +try: + import contextvars # noqa +except ImportError: + from ddtrace.vendor import contextvars # type: ignore # noqa + + CONTEXTVARS_IS_AVAILABLE = False +else: + CONTEXTVARS_IS_AVAILABLE = True + + +try: + from pep562 import Pep562 # noqa + + def ensure_pep562(module_name): + # type: (str) -> None + if sys.version_info < (3, 7): + Pep562(module_name) + + +except ImportError: + + def ensure_pep562(module_name): + # type: (str) -> None + pass + + +def maybe_stringify(obj): + # type: (Any) -> Optional[str] + if obj is not None: + return stringify(obj) + return None diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/debug.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/debug.py new file mode 100644 index 000000000..75a8857c7 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/debug.py @@ -0,0 +1,247 @@ +import datetime +import logging +import os +import platform +import re +import sys +from typing import Any +from typing import Dict +from typing import TYPE_CHECKING +from typing import Union + +import ddtrace +from ddtrace.internal.writer import AgentWriter +from ddtrace.internal.writer import LogWriter +from ddtrace.sampler import DatadogSampler + +from .logger import get_logger + + +if TYPE_CHECKING: + from ddtrace import Tracer + + +logger = get_logger(__name__) + + +def in_venv(): + # type: () -> bool + # Works with both venv and virtualenv + # https://stackoverflow.com/a/42580137 + return ( + "VIRTUAL_ENV" in os.environ + or hasattr(sys, "real_prefix") + or (hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix) + ) + + +def tags_to_str(tags): + # type: (Dict[str, Any]) -> str + # Turn a dict of tags to a string "k1:v1,k2:v2,..." + return ",".join(["%s:%s" % (k, v) for k, v in tags.items()]) + + +def collect(tracer): + # type: (Tracer) -> Dict[str, Any] + """Collect system and library information into a serializable dict.""" + + import pkg_resources + + from ddtrace.internal.runtime.runtime_metrics import RuntimeWorker + + if isinstance(tracer.writer, LogWriter): + agent_url = "AGENTLESS" + agent_error = None + elif isinstance(tracer.writer, AgentWriter): + writer = tracer.writer + agent_url = writer.agent_url + try: + writer.write([]) + writer.flush_queue(raise_exc=True) + except Exception as e: + agent_error = "Agent not reachable at %s. Exception raised: %s" % (agent_url, str(e)) + else: + agent_error = None + else: + agent_url = "CUSTOM" + agent_error = None + + sampler_rules = None + if isinstance(tracer.sampler, DatadogSampler): + sampler_rules = [str(rule) for rule in tracer.sampler.rules] + + is_venv = in_venv() + + packages_available = {p.project_name: p.version for p in pkg_resources.working_set} + integration_configs = {} # type: Dict[str, Union[Dict[str, Any], str]] + for module, enabled in ddtrace.monkey.PATCH_MODULES.items(): + # TODO: this check doesn't work in all cases... we need a mapping + # between the module and the library name. + module_available = module in packages_available + module_instrumented = module in ddtrace.monkey._PATCHED_MODULES + module_imported = module in sys.modules + + if enabled: + # Note that integration configs aren't added until the integration + # module is imported. This typically occurs as a side-effect of + # patch(). + # This also doesn't load work in all cases since we don't always + # name the configuration entry the same as the integration module + # name :/ + config = ddtrace.config._config.get(module, "N/A") + else: + config = None + + if module_available: + integration_configs[module] = dict( + enabled=enabled, + instrumented=module_instrumented, + module_available=module_available, + module_version=packages_available[module], + module_imported=module_imported, + config=config, + ) + else: + # Use N/A here to avoid the additional clutter of an entire + # config dictionary for a module that isn't available. + integration_configs[module] = "N/A" + + pip_version = packages_available.get("pip", "N/A") + + return dict( + # Timestamp UTC ISO 8601 + date=datetime.datetime.utcnow().isoformat(), + # eg. "Linux", "Darwin" + os_name=platform.system(), + # eg. 12.5.0 + os_version=platform.release(), + is_64_bit=sys.maxsize > 2 ** 32, + architecture=platform.architecture()[0], + vm=platform.python_implementation(), + version=ddtrace.__version__, + lang="python", + lang_version=platform.python_version(), + pip_version=pip_version, + in_virtual_env=is_venv, + agent_url=agent_url, + agent_error=agent_error, + env=ddtrace.config.env or "", + is_global_tracer=tracer == ddtrace.tracer, + enabled_env_setting=os.getenv("DATADOG_TRACE_ENABLED"), + tracer_enabled=tracer.enabled, + sampler_type=type(tracer.sampler).__name__ if tracer.sampler else "N/A", + priority_sampler_type=type(tracer.priority_sampler).__name__ if tracer.priority_sampler else "N/A", + sampler_rules=sampler_rules, + service=ddtrace.config.service or "", + debug=ddtrace.tracer.log.isEnabledFor(logging.DEBUG), + enabled_cli="ddtrace" in os.getenv("PYTHONPATH", ""), + analytics_enabled=ddtrace.config.analytics_enabled, + log_injection_enabled=ddtrace.config.logs_injection, + health_metrics_enabled=ddtrace.config.health_metrics_enabled, + runtime_metrics_enabled=RuntimeWorker.enabled, + dd_version=ddtrace.config.version or "", + priority_sampling_enabled=tracer.priority_sampler is not None, + global_tags=os.getenv("DD_TAGS", ""), + tracer_tags=tags_to_str(tracer.tags), + integrations=integration_configs, + partial_flush_enabled=tracer._partial_flush_enabled, + partial_flush_min_spans=tracer._partial_flush_min_spans, + ) + + +def pretty_collect(tracer, color=True): + class bcolors: + HEADER = "\033[95m" + OKBLUE = "\033[94m" + OKCYAN = "\033[96m" + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + ENDC = "\033[0m" + BOLD = "\033[1m" + + info = collect(tracer) + + info_pretty = """{blue}{bold}Tracer Configurations:{end} + Tracer enabled: {tracer_enabled} + Debug logging: {debug} + Writing traces to: {agent_url} + Agent error: {agent_error} + App Analytics enabled(deprecated): {analytics_enabled} + Log injection enabled: {log_injection_enabled} + Health metrics enabled: {health_metrics_enabled} + Priority sampling enabled: {priority_sampling_enabled} + Partial flushing enabled: {partial_flush_enabled} + Partial flush minimum number of spans: {partial_flush_min_spans} + {green}{bold}Tagging:{end} + DD Service: {service} + DD Env: {env} + DD Version: {dd_version} + Global Tags: {global_tags} + Tracer Tags: {tracer_tags}""".format( + tracer_enabled=info.get("tracer_enabled"), + debug=info.get("debug"), + agent_url=info.get("agent_url") or "Not writing at the moment, is your tracer running?", + agent_error=info.get("agent_error") or "None", + analytics_enabled=info.get("analytics_enabled"), + log_injection_enabled=info.get("log_injection_enabled"), + health_metrics_enabled=info.get("health_metrics_enabled"), + priority_sampling_enabled=info.get("priority_sampling_enabled"), + partial_flush_enabled=info.get("partial_flush_enabled"), + partial_flush_min_spans=info.get("partial_flush_min_spans") or "Not set", + service=info.get("service") or "None", + env=info.get("env") or "None", + dd_version=info.get("dd_version") or "None", + global_tags=info.get("global_tags") or "None", + tracer_tags=info.get("tracer_tags") or "None", + blue=bcolors.OKBLUE, + green=bcolors.OKGREEN, + bold=bcolors.BOLD, + end=bcolors.ENDC, + ) + + summary = "{0}{1}Summary{2}".format(bcolors.OKCYAN, bcolors.BOLD, bcolors.ENDC) + + if info.get("agent_error"): + summary += ( + "\n\n{fail}ERROR: It looks like you have an agent error: '{agent_error}'\n If you're experiencing" + " a connection error, please make sure you've followed the setup for your particular environment so that " + "the tracer and Datadog agent are configured properly to connect, and that the Datadog agent is running: " + "https://ddtrace.readthedocs.io/en/stable/troubleshooting.html#failed-to-send-traces-connectionrefused" + "error" + "\nIf your issue is not a connection error then please reach out to support for further assistance:" + " https://docs.datadoghq.com/help/{end}" + ).format(fail=bcolors.FAIL, agent_error=info.get("agent_error"), end=bcolors.ENDC) + + if not info.get("service"): + summary += ( + "\n\n{warning}WARNING SERVICE NOT SET: It is recommended that a service tag be set for all traced" + " applications. For more information please see" + " https://ddtrace.readthedocs.io/en/stable/troubleshooting.html{end}" + ).format(warning=bcolors.WARNING, end=bcolors.ENDC) + + if not info.get("env"): + summary += ( + "\n\n{warning}WARNING ENV NOT SET: It is recommended that an env tag be set for all traced" + " applications. For more information please see " + "https://ddtrace.readthedocs.io/en/stable/troubleshooting.html{end}" + ).format(warning=bcolors.WARNING, end=bcolors.ENDC) + + if not info.get("dd_version"): + summary += ( + "\n\n{warning}WARNING VERSION NOT SET: It is recommended that a version tag be set for all traced" + " applications. For more information please see" + " https://ddtrace.readthedocs.io/en/stable/troubleshooting.html{end}" + ).format(warning=bcolors.WARNING, end=bcolors.ENDC) + + info_pretty += "\n\n" + summary + + if color is False: + return escape_ansi(info_pretty) + + return info_pretty + + +def escape_ansi(line): + ansi_escape = re.compile(r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]") + return ansi_escape.sub("", line) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/dogstatsd.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/dogstatsd.py new file mode 100644 index 000000000..55e4e0af9 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/dogstatsd.py @@ -0,0 +1,27 @@ +from typing import Optional + +from ddtrace.internal.compat import parse +from ddtrace.vendor.dogstatsd import DogStatsd +from ddtrace.vendor.dogstatsd import base + + +def get_dogstatsd_client(url): + # type: (str) -> Optional[DogStatsd] + if not url: + return None + + # url can be either of the form `udp://:` or `unix://` + # also support without url scheme included + if url.startswith("/"): + url = "unix://" + url + elif "://" not in url: + url = "udp://" + url + + parsed = parse.urlparse(url) + + if parsed.scheme == "unix": + return DogStatsd(socket_path=parsed.path) + elif parsed.scheme == "udp": + return DogStatsd(host=parsed.hostname, port=base.DEFAULT_PORT if parsed.port is None else parsed.port) + + raise ValueError("Unknown scheme `%s` for DogStatsD URL `{}`".format(parsed.scheme)) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/encoding.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/encoding.py new file mode 100644 index 000000000..6fd136ec0 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/encoding.py @@ -0,0 +1,96 @@ +import json +from typing import Any +from typing import Dict +from typing import List +from typing import Optional +from typing import TYPE_CHECKING + +from ._encoding import ListStringTable +from ._encoding import MsgpackEncoderV03 +from ._encoding import MsgpackEncoderV05 +from .logger import get_logger + + +__all__ = ["MsgpackEncoderV03", "MsgpackEncoderV05", "ListStringTable", "Encoder"] + + +if TYPE_CHECKING: + from ..span import Span + + +log = get_logger(__name__) + + +class _EncoderBase(object): + """ + Encoder interface that provides the logic to encode traces and service. + """ + + def encode_traces(self, traces): + # type: (List[List[Span]]) -> str + """ + Encodes a list of traces, expecting a list of items where each items + is a list of spans. Before dump the string in a serialized format all + traces are normalized, calling the ``to_dict()`` method. The traces + nesting is not changed. + + :param traces: A list of traces that should be serialized + """ + normalized_traces = [[span.to_dict() for span in trace] for trace in traces] + return self.encode(normalized_traces) + + @staticmethod + def encode(obj): + """ + Defines the underlying format used during traces or services encoding. + This method must be implemented and should only be used by the internal functions. + """ + raise NotImplementedError + + +class JSONEncoder(_EncoderBase): + content_type = "application/json" + + @staticmethod + def encode(obj): + # type: (Any) -> str + return json.dumps(obj) + + +class JSONEncoderV2(JSONEncoder): + """ + JSONEncoderV2 encodes traces to the new intake API format. + """ + + content_type = "application/json" + + def encode_traces(self, traces): + # type: (List[List[Span]]) -> str + normalized_traces = [[JSONEncoderV2._convert_span(span) for span in trace] for trace in traces] + return self.encode({"traces": normalized_traces}) + + @staticmethod + def _convert_span(span): + # type: (Span) -> Dict[str, Any] + sp = span.to_dict() + sp["trace_id"] = JSONEncoderV2._encode_id_to_hex(sp.get("trace_id")) + sp["parent_id"] = JSONEncoderV2._encode_id_to_hex(sp.get("parent_id")) + sp["span_id"] = JSONEncoderV2._encode_id_to_hex(sp.get("span_id")) + return sp + + @staticmethod + def _encode_id_to_hex(dd_id): + # type: (Optional[int]) -> str + if not dd_id: + return "0000000000000000" + return "%0.16X" % int(dd_id) + + @staticmethod + def _decode_id_to_hex(hex_id): + # type: (Optional[str]) -> int + if not hex_id: + return 0 + return int(hex_id, 16) + + +Encoder = MsgpackEncoderV03 diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/forksafe.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/forksafe.py new file mode 100644 index 000000000..62a8371e4 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/forksafe.py @@ -0,0 +1,137 @@ +""" +An API to provide fork-safe functions. +""" +import logging +import os +import threading +import typing +import weakref + +from ddtrace.vendor import wrapt + + +log = logging.getLogger(__name__) + + +_registry = [] # type: typing.List[typing.Callable[[], None]] + +# Some integrations might require after-fork hooks to be executed after the +# actual call to os.fork with earlier versions of Python (<= 3.6), else issues +# like SIGSEGV will occur. Setting this to True will cause the after-fork hooks +# to be executed after the actual fork, which seems to prevent the issue. +_soft = False + + +def ddtrace_after_in_child(): + # type: () -> None + global _registry + + # DEV: we make a copy of the registry to prevent hook execution from + # introducing new hooks, potentially causing an infinite loop. + for hook in list(_registry): + try: + hook() + except Exception: + # Mimic the behaviour of Python's fork hooks. + log.exception("Exception ignored in forksafe hook %r", hook) + + +def register(after_in_child): + # type: (typing.Callable[[], None]) -> typing.Callable[[], None] + """Register a function to be called after fork in the child process. + + Note that ``after_in_child`` will be called in all child processes across + multiple forks unless it is unregistered. + """ + _registry.append(after_in_child) + return after_in_child + + +def unregister(after_in_child): + # type: (typing.Callable[[], None]) -> None + """Unregister a function to be called after fork in the child process. + + Raises `ValueError` if the function was not registered. + """ + _registry.remove(after_in_child) + + +if hasattr(os, "register_at_fork"): + os.register_at_fork(after_in_child=ddtrace_after_in_child) +elif hasattr(os, "fork"): + # DEV: This "should" be the correct way of implementing this, but it doesn't + # work if hooks create new threads. + _threading_after_fork = threading._after_fork # type: ignore + + def _after_fork(): + # type: () -> None + _threading_after_fork() + if not _soft: + ddtrace_after_in_child() + + threading._after_fork = _after_fork # type: ignore[attr-defined] + + # DEV: If hooks create threads, we should do this instead. + _os_fork = os.fork + + def _fork(): + pid = _os_fork() + if pid == 0 and _soft: + ddtrace_after_in_child() + return pid + + os.fork = _fork + +_resetable_objects = weakref.WeakSet() # type: weakref.WeakSet[ResetObject] + + +def _reset_objects(): + # type: (...) -> None + for obj in list(_resetable_objects): + try: + obj._reset_object() + except Exception: + log.exception("Exception ignored in object reset forksafe hook %r", obj) + + +register(_reset_objects) + + +_T = typing.TypeVar("_T") + + +class ResetObject(wrapt.ObjectProxy, typing.Generic[_T]): + """An object wrapper object that is fork-safe and resets itself after a fork. + + When a Python process forks, a Lock can be in any state, locked or not, by any thread. Since after fork all threads + are gone, Lock objects needs to be reset. CPython does this with an internal `threading._after_fork` function. We + use the same mechanism here. + + """ + + def __init__( + self, wrapped_class # type: typing.Type[_T] + ): + # type: (...) -> None + super(ResetObject, self).__init__(wrapped_class()) + self._self_wrapped_class = wrapped_class + _resetable_objects.add(self) + + def _reset_object(self): + # type: (...) -> None + self.__wrapped__ = self._self_wrapped_class() + + +def Lock(): + # type: (...) -> ResetObject[threading.Lock] + return ResetObject(threading.Lock) + + +def RLock(): + # type: (...) -> ResetObject[threading.RLock] + return ResetObject(threading.RLock) + + +def Event(): + # type: (...) -> ResetObject[threading.Event] + return ResetObject(threading.Event) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/hostname.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/hostname.py new file mode 100644 index 000000000..cea2c6162 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/hostname.py @@ -0,0 +1,13 @@ +import socket +from typing import Optional + + +_hostname = None # type: Optional[str] + + +def get_hostname(): + # type: () -> str + global _hostname + if not _hostname: + _hostname = socket.gethostname() + return _hostname diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/http.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/http.py new file mode 100644 index 000000000..c8e6c772d --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/http.py @@ -0,0 +1,36 @@ +from ddtrace.internal.compat import httplib +from ddtrace.internal.compat import parse + + +class BasePathMixin(httplib.HTTPConnection, object): + """ + Mixin for HTTPConnection to insert a base path to requested URLs + """ + + _base_path = "/" # type: str + + def putrequest(self, method, url, skip_host=False, skip_accept_encoding=False): + # type: (str, str, bool, bool) -> None + url = parse.urljoin(self._base_path, url) + return super(BasePathMixin, self).putrequest( + method, url, skip_host=skip_host, skip_accept_encoding=skip_accept_encoding + ) + + @classmethod + def with_base_path(cls, *args, **kwargs): + base_path = kwargs.pop("base_path", None) + obj = cls(*args, **kwargs) + obj._base_path = base_path + return obj + + +class HTTPConnection(BasePathMixin, httplib.HTTPConnection): + """ + httplib.HTTPConnection wrapper to add a base path to requested URLs + """ + + +class HTTPSConnection(BasePathMixin, httplib.HTTPSConnection): + """ + httplib.HTTPSConnection wrapper to add a base path to requested URLs + """ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/logger.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/logger.py new file mode 100644 index 000000000..459939975 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/logger.py @@ -0,0 +1,176 @@ +import collections +import logging +import os +from typing import Any +from typing import DefaultDict +from typing import Tuple +from typing import cast + +from ..utils.deprecation import deprecation +from ..utils.formats import get_env + + +def get_logger(name): + # type: (str) -> DDLogger + """ + Retrieve or create a ``DDLogger`` instance. + + This function mirrors the behavior of `logging.getLogger`. + + If no logger with the provided name has been fetched before then + a new one is created. + + If a previous logger has been created then it is returned. + + DEV: We do not want to mess with `logging.setLoggerClass()` + That will totally mess with the user's loggers, we want + just our own, selective loggers to be DDLoggers + + :param name: The name of the logger to fetch or create + :type name: str + :return: The logger instance + :rtype: ``DDLogger`` + """ + # DEV: `logging.Logger.manager` refers to the single root `logging.Manager` instance + # https://github.com/python/cpython/blob/48769a28ad6ef4183508951fa6a378531ace26a4/Lib/logging/__init__.py#L1824-L1826 # noqa + manager = logging.Logger.manager + + # If the logger does not exist yet, create it + # DEV: `Manager.loggerDict` is a dict mapping logger name to logger + # DEV: This is a simplified version of `logging.Manager.getLogger` + # https://github.com/python/cpython/blob/48769a28ad6ef4183508951fa6a378531ace26a4/Lib/logging/__init__.py#L1221-L1253 # noqa + if name not in manager.loggerDict: + manager.loggerDict[name] = DDLogger(name=name) + + # Get our logger + logger = cast(DDLogger, manager.loggerDict[name]) + + # If this log manager has a `_fixupParents` method then call it on our logger + # DEV: This helper is used to ensure our logger has an appropriate `Logger.parent` set, + # without this then we cannot take advantage of the root loggers handlers + # https://github.com/python/cpython/blob/7c7839329c2c66d051960ab1df096aed1cc9343e/Lib/logging/__init__.py#L1272-L1294 # noqa + # DEV: `_fixupParents` has been around for awhile, but add the `hasattr` guard... just in case. + if hasattr(manager, "_fixupParents"): + manager._fixupParents(logger) # type: ignore[attr-defined] + + # Return our logger + return logger + + +def hasHandlers(self): + # type: (DDLogger) -> bool + """ + See if this logger has any handlers configured. + Loop through all handlers for this logger and its parents in the + logger hierarchy. Return True if a handler was found, else False. + Stop searching up the hierarchy whenever a logger with the "propagate" + attribute set to zero is found - that will be the last logger which + is checked for the existence of handlers. + + https://github.com/python/cpython/blob/8f192d12af82c4dc40730bf59814f6a68f68f950/Lib/logging/__init__.py#L1629 + """ + c = self + rv = False + while c: + if c.handlers: + rv = True + break + if not c.propagate: + break + else: + c = c.parent # type: ignore + return rv + + +class DDLogger(logging.Logger): + """ + Custom rate limited logger used by ``ddtrace`` + + This logger class is used to rate limit the output of + log messages from within the ``ddtrace`` package. + """ + + __slots__ = ("buckets", "rate_limit") + + # Named tuple used for keeping track of a log lines current time bucket and the number of log lines skipped + LoggingBucket = collections.namedtuple("LoggingBucket", ("bucket", "skipped")) + + def __init__(self, *args, **kwargs): + # type: (*Any, **Any) -> None + """Constructor for ``DDLogger``""" + super(DDLogger, self).__init__(*args, **kwargs) + + # Dict to keep track of the current time bucket per name/level/pathname/lineno + self.buckets = collections.defaultdict( + lambda: DDLogger.LoggingBucket(0, 0) + ) # type: DefaultDict[Tuple[str, int, str, int], DDLogger.LoggingBucket] + + # Allow 1 log record per name/level/pathname/lineno every 60 seconds by default + # Allow configuring via `DD_TRACE_LOGGING_RATE` + # DEV: `DD_TRACE_LOGGING_RATE=0` means to disable all rate limiting + rate_limit = os.getenv("DD_TRACE_LOGGING_RATE", default=None) + if rate_limit is None: + # DEV: If not set, look at the deprecated (DD/DATADOG)_LOGGING_RATE_LIMIT + rate_limit = get_env("logging", "rate_limit") + if rate_limit is not None: + deprecation( + name="DD_LOGGING_RATE_LIMIT", + message="Use `DD_TRACE_LOGGING_RATE` instead", + version="1.0.0", + ) + + if rate_limit is not None: + self.rate_limit = int(rate_limit) + else: + self.rate_limit = 60 + + def handle(self, record): + # type: (logging.LogRecord) -> None + """ + Function used to call the handlers for a log line. + + This implementation will first determine if this log line should + be logged or rate limited, and then call the base ``logging.Logger.handle`` + function if it should be logged + + DEV: This method has all of it's code inlined to reduce on functions calls + + :param record: The log record being logged + :type record: ``logging.LogRecord`` + """ + # If rate limiting has been disabled (`DD_TRACE_LOGGING_RATE=0`) then apply no rate limit + # If the logging is in debug, then do not apply any limits to any log + if not self.rate_limit or self.getEffectiveLevel() == logging.DEBUG: + super(DDLogger, self).handle(record) + return + + # Allow 1 log record by name/level/pathname/lineno every X seconds + # DEV: current unix time / rate (e.g. 300 seconds) = time bucket + # int(1546615098.8404942 / 300) = 515538 + # DEV: LogRecord `created` is a unix timestamp/float + # DEV: LogRecord has `levelname` and `levelno`, we want `levelno` e.g. `logging.DEBUG = 10` + current_bucket = int(record.created / self.rate_limit) + + # Limit based on logger name, record level, filename, and line number + # ('ddtrace.writer', 'DEBUG', '../site-packages/ddtrace/writer.py', 137) + # This way each unique log message can get logged at least once per time period + # DEV: LogRecord has `levelname` and `levelno`, we want `levelno` e.g. `logging.DEBUG = 10` + key = (record.name, record.levelno, record.pathname, record.lineno) + + # Only log this message if the time bucket has changed from the previous time we ran + logging_bucket = self.buckets[key] + if logging_bucket.bucket != current_bucket: + # Append count of skipped messages if we have skipped some since our last logging + if logging_bucket.skipped: + record.msg = "{}, %s additional messages skipped".format(record.msg) + record.args = record.args + (logging_bucket.skipped,) # type: ignore + + # Reset our bucket + self.buckets[key] = DDLogger.LoggingBucket(current_bucket, 0) + + # Call the base handle to actually log this record + super(DDLogger, self).handle(record) + else: + # Increment the count of records we have skipped + # DEV: `self.buckets[key]` is a tuple which is immutable so recreate instead + self.buckets[key] = DDLogger.LoggingBucket(logging_bucket.bucket, logging_bucket.skipped + 1) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/nogevent.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/nogevent.py new file mode 100644 index 000000000..51328ba31 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/nogevent.py @@ -0,0 +1,84 @@ +# -*- encoding: utf-8 -*- +"""This files exposes non-gevent Python original functions.""" +import threading + +import attr +import six + +from ddtrace.internal import compat +from ddtrace.internal import forksafe + + +try: + import gevent.monkey +except ImportError: + + def get_original(module, func): + return getattr(__import__(module), func) + + def is_module_patched(module): + return False + + +else: + get_original = gevent.monkey.get_original + is_module_patched = gevent.monkey.is_module_patched + + +sleep = get_original("time", "sleep") + +try: + # Python ≥ 3.8 + threading_get_native_id = get_original("threading", "get_native_id") +except AttributeError: + threading_get_native_id = None + +start_new_thread = get_original(six.moves._thread.__name__, "start_new_thread") +thread_get_ident = get_original(six.moves._thread.__name__, "get_ident") +Thread = get_original("threading", "Thread") +Lock = get_original("threading", "Lock") + + +if is_module_patched("threading"): + + @attr.s + class DoubleLock(object): + """A lock that prevent concurrency from a gevent coroutine and from a threading.Thread at the same time.""" + + # This is a gevent-patched threading.Lock (= a gevent Lock) + _lock = attr.ib(factory=forksafe.Lock, init=False, repr=False) + # This is a unpatched threading.Lock (= a real threading.Lock) + _thread_lock = attr.ib(factory=lambda: forksafe.ResetObject(Lock), init=False, repr=False) + + def acquire(self): + # type: () -> None + # You cannot acquire a gevent-lock from another thread if it has been acquired already: + # make sure we exclude the gevent-lock from being acquire by another thread by using a thread-lock first. + self._thread_lock.acquire() + self._lock.acquire() + + def release(self): + # type: () -> None + self._lock.release() + self._thread_lock.release() + + def __enter__(self): + # type: () -> DoubleLock + self.acquire() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.release() + + +else: + DoubleLock = threading.Lock # type: ignore[misc,assignment] + + +if is_module_patched("threading"): + # NOTE: bold assumption: this module is always imported by the MainThread. + # The python `threading` module makes that assumption and it's beautiful we're going to do the same. + # We don't have the choice has we can't access the original MainThread + main_thread_id = thread_get_ident() +else: + main_thread_id = compat.main_thread.ident diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/periodic.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/periodic.py new file mode 100644 index 000000000..4a4861aac --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/periodic.py @@ -0,0 +1,244 @@ +# -*- encoding: utf-8 -*- +import sys +import threading +import time +import typing + +import attr + +from ddtrace.internal import nogevent +from ddtrace.internal import service + +from . import forksafe + + +class PeriodicThread(threading.Thread): + """Periodic thread. + + This class can be used to instantiate a worker thread that will run its `run_periodic` function every `interval` + seconds. + + """ + + _ddtrace_profiling_ignore = True + + def __init__( + self, + interval, # type: float + target, # type: typing.Callable[[], typing.Any] + name=None, # type: typing.Optional[str] + on_shutdown=None, # type: typing.Optional[typing.Callable[[], typing.Any]] + ): + # type: (...) -> None + """Create a periodic thread. + + :param interval: The interval in seconds to wait between execution of the periodic function. + :param target: The periodic function to execute every interval. + :param name: The name of the thread. + :param on_shutdown: The function to call when the thread shuts down. + """ + super(PeriodicThread, self).__init__(name=name) + self._target = target + self._on_shutdown = on_shutdown + self.interval = interval + self.quit = forksafe.Event() + self.daemon = True + + def stop(self): + """Stop the thread.""" + # NOTE: make sure the thread is alive before using self.quit: + # 1. self.quit is Lock-based + # 2. if we're a child trying to stop a Thread, + # the Lock might have been locked in a parent process while forking so that'd block forever + if self.is_alive(): + self.quit.set() + + def run(self): + """Run the target function periodically.""" + while not self.quit.wait(self.interval): + # DEV: Some frameworks, like e.g. gevent, seem to resuscitate some + # of the threads that were running prior to the fork of the worker + # processes. These threads are normally created via the native API + # and are exposed to the child process as _DummyThreads. We check + # whether the current thread is no longer an instance of the + # original thread class to prevent it from running in the child + # process while the state copied over from the parent is being + # cleaned up. The restarting of the thread is responsibility to the + # registered forksafe hooks. + if not isinstance(threading.current_thread(), self.__class__): + break + self._target() + if self._on_shutdown is not None: + self._on_shutdown() + + +class _GeventPeriodicThread(PeriodicThread): + """Periodic thread. + + This class can be used to instantiate a worker thread that will run its `run_periodic` function every `interval` + seconds. + + """ + + # That's the value Python 2 uses in its `threading` module + SLEEP_INTERVAL = 0.005 + + def __init__(self, interval, target, name=None, on_shutdown=None): + """Create a periodic thread. + + :param interval: The interval in seconds to wait between execution of the periodic function. + :param target: The periodic function to execute every interval. + :param name: The name of the thread. + :param on_shutdown: The function to call when the thread shuts down. + """ + super(_GeventPeriodicThread, self).__init__(interval, target, name, on_shutdown) + self._tident = None + self._periodic_started = False + self._periodic_stopped = False + + def _reset_internal_locks(self, is_alive=False): + # Called by Python via `threading._after_fork` + self._periodic_stopped = True + + @property + def ident(self): + return self._tident + + def start(self): + """Start the thread.""" + self.quit = False + if self._tident is not None: + raise RuntimeError("threads can only be started once") + self._tident = nogevent.start_new_thread(self.run, tuple()) + if nogevent.threading_get_native_id: + self._native_id = nogevent.threading_get_native_id() + + # Wait for the thread to be started to avoid race conditions + while not self._periodic_started: + time.sleep(self.SLEEP_INTERVAL) + + def is_alive(self): + return not self._periodic_stopped and self._periodic_started + + def join(self, timeout=None): + # FIXME: handle the timeout argument + while self.is_alive(): + time.sleep(self.SLEEP_INTERVAL) + + def stop(self): + """Stop the thread.""" + self.quit = True + + def run(self): + """Run the target function periodically.""" + # Do not use the threading._active_limbo_lock here because it's a gevent lock + threading._active[self._tident] = self + + self._periodic_started = True + + try: + while self.quit is False: + self._target() + slept = 0 + while self.quit is False and slept < self.interval: + nogevent.sleep(self.SLEEP_INTERVAL) + slept += self.SLEEP_INTERVAL + if self._on_shutdown is not None: + self._on_shutdown() + except Exception: + # Exceptions might happen during interpreter shutdown. + # We're mimicking what `threading.Thread` does in daemon mode, we ignore them. + # See `threading.Thread._bootstrap` for details. + if sys is not None: + raise + finally: + try: + self._periodic_stopped = True + del threading._active[self._tident] + except Exception: + # Exceptions might happen during interpreter shutdown. + # We're mimicking what `threading.Thread` does in daemon mode, we ignore them. + # See `threading.Thread._bootstrap` for details. + if sys is not None: + raise + + +def PeriodicRealThreadClass(): + # type: () -> typing.Type[PeriodicThread] + """Return a PeriodicThread class based on the underlying thread implementation (native, gevent, etc). + + The returned class works exactly like ``PeriodicThread``, except that it runs on a *real* OS thread. Be aware that + this might be tricky in e.g. the gevent case, where ``Lock`` object must not be shared with the ``MainThread`` + (otherwise it'd dead lock). + + """ + if nogevent.is_module_patched("threading"): + return _GeventPeriodicThread + return PeriodicThread + + +@attr.s(eq=False) +class PeriodicService(service.Service): + """A service that runs periodically.""" + + _interval = attr.ib(type=float) + _worker = attr.ib(default=None, init=False, repr=False) + + _real_thread = False + "Class variable to override if the service should run in a real OS thread." + + @property + def interval(self): + # type: (...) -> float + return self._interval + + @interval.setter + def interval( + self, value # type: float + ): + # type: (...) -> None + self._interval = value + # Update the interval of the PeriodicThread based on ours + if self._worker: + self._worker.interval = value + + def _start_service( + self, + *args, # type: typing.Any + **kwargs # type: typing.Any + ): + # type: (...) -> None + """Start the periodic service.""" + periodic_thread_class = PeriodicRealThreadClass() if self._real_thread else PeriodicThread + self._worker = periodic_thread_class( + self.interval, + target=self.periodic, + name="%s:%s" % (self.__class__.__module__, self.__class__.__name__), + on_shutdown=self.on_shutdown, + ) + self._worker.start() + + def _stop_service( + self, + *args, # type: typing.Any + **kwargs # type: typing.Any + ): + # type: (...) -> None + """Stop the periodic collector.""" + self._worker.stop() + super(PeriodicService, self)._stop_service(*args, **kwargs) + + def join( + self, timeout=None # type: typing.Optional[float] + ): + # type: (...) -> None + if self._worker: + self._worker.join(timeout) + + @staticmethod + def on_shutdown(): + pass + + def periodic(self): + # type: (...) -> None + pass diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/processor/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/processor/__init__.py new file mode 100644 index 000000000..cd2f6d77c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/processor/__init__.py @@ -0,0 +1,53 @@ +import abc + +import attr +import six + +from ddtrace import Span +from ddtrace.internal.logger import get_logger + + +log = get_logger(__name__) + + +@attr.s +class SpanProcessor(six.with_metaclass(abc.ABCMeta)): + """A Processor is used to process spans as they are created and finished by a tracer.""" + + def __attrs_post_init__(self): + # type: () -> None + """Default post initializer which logs the representation of the + Processor at the ``logging.DEBUG`` level. + + The representation can be modified with the ``repr`` argument to the + attrs attribute:: + + @attr.s + class MyProcessor(Processor): + field_to_include = attr.ib(repr=True) + field_to_exclude = attr.ib(repr=False) + """ + log.debug("initialized processor %r", self) + + @abc.abstractmethod + def on_span_start(self, span): + # type: (Span) -> None + """Called when a span is started. + + This method is useful for making upfront decisions on spans. + + For example, a sampling decision can be made when the span is created + based on its resource name. + """ + pass + + @abc.abstractmethod + def on_span_finish(self, span): + # type: (Span) -> None + """Called with the result of any previous processors or initially with + the finishing span when a span finishes. + + It can return any data which will be passed to any processors that are + applied afterwards. + """ + pass diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/processor/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/processor/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..3955133d5 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/processor/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/processor/__pycache__/trace.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/processor/__pycache__/trace.cpython-38.pyc new file mode 100644 index 000000000..2632953fc Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/processor/__pycache__/trace.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/processor/trace.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/processor/trace.py new file mode 100644 index 000000000..45b146759 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/processor/trace.py @@ -0,0 +1,166 @@ +import abc +from collections import defaultdict +import threading +from typing import DefaultDict +from typing import Iterable +from typing import List +from typing import Optional + +import attr +import six + +from ddtrace.internal.logger import get_logger +from ddtrace.internal.processor import SpanProcessor +from ddtrace.internal.writer import TraceWriter +from ddtrace.span import Span + + +log = get_logger(__name__) + + +@attr.s +class TraceProcessor(six.with_metaclass(abc.ABCMeta)): + def __attrs_post_init__(self): + # type: () -> None + """Default post initializer which logs the representation of the + TraceProcessor at the ``logging.DEBUG`` level. + + The representation can be modified with the ``repr`` argument to the + attrs attribute:: + + @attr.s + class MyTraceProcessor(TraceProcessor): + field_to_include = attr.ib(repr=True) + field_to_exclude = attr.ib(repr=False) + """ + log.debug("initialized trace processor %r", self) + + @abc.abstractmethod + def process_trace(self, trace): + # type: (List[Span]) -> Optional[List[Span]] + """Processes a trace. + + ``None`` can be returned to prevent the trace from being further + processed. + """ + pass + + +@attr.s +class TraceSamplingProcessor(TraceProcessor): + """Processor that keeps traces that have sampled spans. If all spans + are unsampled then ``None`` is returned. + + Note that this processor is only effective if complete traces are sent. If + the spans of a trace are divided in separate lists then it's possible that + parts of the trace are unsampled when the whole trace should be sampled. + """ + + def process_trace(self, trace): + # type: (List[Span]) -> Optional[List[Span]] + if trace: + for span in trace: + if span.sampled: + return trace + + log.debug("dropping trace %d with %d spans", trace[0].trace_id, len(trace)) + + return None + + +@attr.s +class TraceTagsProcessor(TraceProcessor): + """Processor that applies trace-level tags to the trace.""" + + def process_trace(self, trace): + # type: (List[Span]) -> Optional[List[Span]] + if not trace: + return trace + + chunk_root = trace[0] + ctx = chunk_root._context + if not ctx: + return trace + + ctx._update_tags(chunk_root) + return trace + + +@attr.s +class SpanAggregator(SpanProcessor): + """Processor that aggregates spans together by trace_id and writes the + spans to the provided writer when: + - The collection is assumed to be complete. A collection of spans is + assumed to be complete if all the spans that have been created with + the trace_id have finished; or + - A minimum threshold of spans (``partial_flush_min_spans``) have been + finished in the collection and ``partial_flush_enabled`` is True. + """ + + @attr.s + class _Trace(object): + spans = attr.ib(default=attr.Factory(list)) # type: List[Span] + num_finished = attr.ib(type=int, default=0) # type: int + + _partial_flush_enabled = attr.ib(type=bool) + _partial_flush_min_spans = attr.ib(type=int) + _trace_processors = attr.ib(type=Iterable[TraceProcessor]) + _writer = attr.ib(type=TraceWriter) + _traces = attr.ib( + factory=lambda: defaultdict(lambda: SpanAggregator._Trace()), + init=False, + type=DefaultDict[int, "_Trace"], + repr=False, + ) + _lock = attr.ib(init=False, factory=threading.Lock, repr=False) + + def on_span_start(self, span): + # type: (Span) -> None + with self._lock: + trace = self._traces[span.trace_id] + trace.spans.append(span) + + def on_span_finish(self, span): + # type: (Span) -> None + with self._lock: + trace = self._traces[span.trace_id] + trace.num_finished += 1 + should_partial_flush = self._partial_flush_enabled and trace.num_finished >= self._partial_flush_min_spans + if trace.num_finished == len(trace.spans) or should_partial_flush: + trace_spans = trace.spans + trace.spans = [] + if trace.num_finished < len(trace_spans): + finished = [] + for s in trace_spans: + if s.finished: + finished.append(s) + else: + trace.spans.append(s) + + else: + finished = trace_spans + + num_finished = len(finished) + + if should_partial_flush: + log.debug("Partially flushing %d spans for trace %d", num_finished, span.trace_id) + + trace.num_finished -= num_finished + + if len(trace.spans) == 0: + del self._traces[span.trace_id] + + spans = finished # type: Optional[List[Span]] + for tp in self._trace_processors: + try: + if spans is None: + return + spans = tp.process_trace(spans) + except Exception: + log.error("error applying processor %r", tp, exc_info=True) + + self._writer.write(spans) + return + + log.debug("trace %d has %d spans, %d finished", span.trace_id, len(trace.spans), trace.num_finished) + return None diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/rate_limiter.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/rate_limiter.py new file mode 100644 index 000000000..860fc4330 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/rate_limiter.py @@ -0,0 +1,158 @@ +from __future__ import division + +import threading +from typing import Optional + +from ..internal import compat + + +class RateLimiter(object): + """ + A token bucket rate limiter implementation + """ + + __slots__ = ( + "_lock", + "current_window", + "last_update", + "max_tokens", + "prev_window_rate", + "rate_limit", + "tokens", + "tokens_allowed", + "tokens_total", + ) + + def __init__(self, rate_limit): + # type: (int) -> None + """ + Constructor for RateLimiter + + :param rate_limit: The rate limit to apply for number of requests per second. + rate limit > 0 max number of requests to allow per second, + rate limit == 0 to disallow all requests, + rate limit < 0 to allow all requests + :type rate_limit: :obj:`int` + """ + self.rate_limit = rate_limit + self.tokens = rate_limit # type: float + self.max_tokens = rate_limit + + self.last_update = compat.monotonic() + + self.current_window = 0 # type: float + self.tokens_allowed = 0 + self.tokens_total = 0 + self.prev_window_rate = None # type: Optional[float] + + self._lock = threading.Lock() + + def is_allowed(self): + # type: () -> bool + """ + Check whether the current request is allowed or not + + This method will also reduce the number of available tokens by 1 + + :returns: Whether the current request is allowed or not + :rtype: :obj:`bool` + """ + # Determine if it is allowed + allowed = self._is_allowed() + # Update counts used to determine effective rate + self._update_rate_counts(allowed) + return allowed + + def _update_rate_counts(self, allowed): + # type: (bool) -> None + now = compat.monotonic() + + # No tokens have been seen yet, start a new window + if not self.current_window: + self.current_window = now + + # If more than 1 second has past since last window, reset + elif now - self.current_window >= 1.0: + # Store previous window's rate to average with current for `.effective_rate` + self.prev_window_rate = self._current_window_rate() + self.tokens_allowed = 0 + self.tokens_total = 0 + self.current_window = now + + # Keep track of total tokens seen vs allowed + if allowed: + self.tokens_allowed += 1 + self.tokens_total += 1 + + def _is_allowed(self): + # type: () -> bool + # Rate limit of 0 blocks everything + if self.rate_limit == 0: + return False + + # Negative rate limit disables rate limiting + elif self.rate_limit < 0: + return True + + # Lock, we need this to be thread safe, it should be shared by all threads + with self._lock: + self._replenish() + + if self.tokens >= 1: + self.tokens -= 1 + return True + + return False + + def _replenish(self): + # type: () -> None + # If we are at the max, we do not need to add any more + if self.tokens == self.max_tokens: + return + + # Add more available tokens based on how much time has passed + now = compat.monotonic() + elapsed = now - self.last_update + self.last_update = now + + # Update the number of available tokens, but ensure we do not exceed the max + self.tokens = min( + self.max_tokens, + self.tokens + (elapsed * self.rate_limit), + ) + + def _current_window_rate(self): + # type: () -> float + # No tokens have been seen, effectively 100% sample rate + # DEV: This is to avoid division by zero error + if not self.tokens_total: + return 1.0 + + # Get rate of tokens allowed + return self.tokens_allowed / self.tokens_total + + @property + def effective_rate(self): + # type: () -> float + """ + Return the effective sample rate of this rate limiter + + :returns: Effective sample rate value 0.0 <= rate <= 1.0 + :rtype: :obj:`float`` + """ + # If we have not had a previous window yet, return current rate + if self.prev_window_rate is None: + return self._current_window_rate() + + return (self._current_window_rate() + self.prev_window_rate) / 2.0 + + def __repr__(self): + return "{}(rate_limit={!r}, tokens={!r}, last_update={!r}, effective_rate={!r})".format( + self.__class__.__name__, + self.rate_limit, + self.tokens, + self.last_update, + self.effective_rate, + ) + + __str__ = __repr__ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__init__.py new file mode 100644 index 000000000..4fbd5f610 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__init__.py @@ -0,0 +1,29 @@ +import uuid + +from .. import forksafe + + +__all__ = [ + "get_runtime_id", +] + + +def _generate_runtime_id(): + return uuid.uuid4().hex + + +_RUNTIME_ID = _generate_runtime_id() + + +@forksafe.register +def _set_runtime_id(): + global _RUNTIME_ID + _RUNTIME_ID = _generate_runtime_id() + + +def get_runtime_id(): + """Return a unique string identifier for this runtime. + + Do not store this identifier as it can change when, e.g., the process forks. + """ + return _RUNTIME_ID diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..2c1fe6c60 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/collector.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/collector.cpython-38.pyc new file mode 100644 index 000000000..6d6301d49 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/collector.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/constants.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/constants.cpython-38.pyc new file mode 100644 index 000000000..06232aaf9 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/constants.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/container.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/container.cpython-38.pyc new file mode 100644 index 000000000..aa2365c36 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/container.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/metric_collectors.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/metric_collectors.cpython-38.pyc new file mode 100644 index 000000000..bb75716ce Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/metric_collectors.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/runtime_metrics.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/runtime_metrics.cpython-38.pyc new file mode 100644 index 000000000..139e818b1 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/runtime_metrics.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/tag_collectors.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/tag_collectors.cpython-38.pyc new file mode 100644 index 000000000..29a1cc418 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/__pycache__/tag_collectors.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/collector.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/collector.py new file mode 100644 index 000000000..db1d1dbe3 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/collector.py @@ -0,0 +1,89 @@ +import importlib +from typing import List +from typing import Optional +from typing import Set +from typing import Tuple + +from ..logger import get_logger + + +log = get_logger(__name__) + + +class ValueCollector(object): + """A basic state machine useful for collecting, caching and updating data + obtained from different Python modules. + + The two primary use-cases are + 1) data loaded once (like tagging information) + 2) periodically updating data sources (like thread count) + + Functionality is provided for requiring and importing modules which may or + may not be installed. + """ + + enabled = True + periodic = False + required_modules = [] # type: List[str] + value = None # type: Optional[List[Tuple[str, str]]] + value_loaded = False + + def __init__(self, enabled=None, periodic=None, required_modules=None): + # type: (Optional[bool], Optional[bool], Optional[List[str]]) -> None + self.enabled = self.enabled if enabled is None else enabled + self.periodic = self.periodic if periodic is None else periodic + self.required_modules = self.required_modules if required_modules is None else required_modules + + self._modules_successfully_loaded = False + self.modules = self._load_modules() + if self._modules_successfully_loaded: + self._on_modules_load() + + def _on_modules_load(self): + """Hook triggered after all required_modules have been successfully loaded.""" + + def _load_modules(self): + modules = {} + try: + for module in self.required_modules: + modules[module] = importlib.import_module(module) + self._modules_successfully_loaded = True + except ImportError: + # DEV: disable collector if we cannot load any of the required modules + self.enabled = False + log.warning('Could not import module "%s" for %s. Disabling collector.', module, self) + return None + return modules + + def collect(self, keys=None): + # type: (Optional[Set[str]]) -> Optional[List[Tuple[str, str]]] + """Returns metrics as collected by `collect_fn`. + + :param keys: The keys of the metrics to collect. + """ + if not self.enabled: + return self.value + + keys = keys or set() + + if not self.periodic and self.value_loaded: + return self.value + + # call underlying collect function and filter out keys not requested + # TODO: provide base method collect_fn() in ValueCollector + self.value = self.collect_fn(keys) # type: ignore[attr-defined] + + # filter values for keys + if len(keys) > 0 and isinstance(self.value, list): + self.value = [(k, v) for (k, v) in self.value if k in keys] + + self.value_loaded = True + return self.value + + def __repr__(self): + return "<{}(enabled={},periodic={},required_modules={})>".format( + self.__class__.__name__, + self.enabled, + self.periodic, + self.required_modules, + ) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/constants.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/constants.py new file mode 100644 index 000000000..892d2a85f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/constants.py @@ -0,0 +1,33 @@ +GC_COUNT_GEN0 = "runtime.python.gc.count.gen0" +GC_COUNT_GEN1 = "runtime.python.gc.count.gen1" +GC_COUNT_GEN2 = "runtime.python.gc.count.gen2" + +THREAD_COUNT = "runtime.python.thread_count" +MEM_RSS = "runtime.python.mem.rss" +# `runtime.python.cpu.time.sys` metric is used to auto-enable runtime metrics dashboards in DD backend +CPU_TIME_SYS = "runtime.python.cpu.time.sys" +CPU_TIME_USER = "runtime.python.cpu.time.user" +CPU_PERCENT = "runtime.python.cpu.percent" +CTX_SWITCH_VOLUNTARY = "runtime.python.cpu.ctx_switch.voluntary" +CTX_SWITCH_INVOLUNTARY = "runtime.python.cpu.ctx_switch.involuntary" + +GC_RUNTIME_METRICS = set([GC_COUNT_GEN0, GC_COUNT_GEN1, GC_COUNT_GEN2]) + +PSUTIL_RUNTIME_METRICS = set( + [THREAD_COUNT, MEM_RSS, CTX_SWITCH_VOLUNTARY, CTX_SWITCH_INVOLUNTARY, CPU_TIME_SYS, CPU_TIME_USER, CPU_PERCENT] +) + +DEFAULT_RUNTIME_METRICS = GC_RUNTIME_METRICS | PSUTIL_RUNTIME_METRICS + +SERVICE = "service" +ENV = "env" +LANG_INTERPRETER = "lang_interpreter" +LANG_VERSION = "lang_version" +LANG = "lang" +TRACER_VERSION = "tracer_version" + +TRACER_TAGS = set([SERVICE, ENV]) + +PLATFORM_TAGS = set([LANG_INTERPRETER, LANG_VERSION, LANG, TRACER_VERSION]) + +DEFAULT_RUNTIME_TAGS = TRACER_TAGS | PLATFORM_TAGS diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/container.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/container.py new file mode 100644 index 000000000..8564ff7f0 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/container.py @@ -0,0 +1,111 @@ +import errno +import re +from typing import Optional + +import attr + +from ..logger import get_logger + + +log = get_logger(__name__) + + +@attr.s(slots=True) +class CGroupInfo(object): + """ + CGroup class for container information parsed from a group cgroup file + """ + + id = attr.ib(default=None) + groups = attr.ib(default=None) + path = attr.ib(default=None) + container_id = attr.ib(default=None) + controllers = attr.ib(default=None) + pod_id = attr.ib(default=None) + + UUID_SOURCE_PATTERN = r"[0-9a-f]{8}[-_][0-9a-f]{4}[-_][0-9a-f]{4}[-_][0-9a-f]{4}[-_][0-9a-f]{12}" + CONTAINER_SOURCE_PATTERN = r"[0-9a-f]{64}" + TASK_PATTERN = r"[0-9a-f]{32}-\d+" + + LINE_RE = re.compile(r"^(\d+):([^:]*):(.+)$") + POD_RE = re.compile(r"pod({0})(?:\.slice)?$".format(UUID_SOURCE_PATTERN)) + CONTAINER_RE = re.compile( + r"(?:.+)?({0}|{1}|{2})(?:\.scope)?$".format(UUID_SOURCE_PATTERN, CONTAINER_SOURCE_PATTERN, TASK_PATTERN) + ) + + @classmethod + def from_line(cls, line): + # type: (str) -> Optional[CGroupInfo] + """ + Parse a new :class:`CGroupInfo` from the provided line + + :param line: A line from a cgroup file (e.g. /proc/self/cgroup) to parse information from + :type line: str + :returns: A :class:`CGroupInfo` object with all parsed data, if the line is valid, otherwise `None` + :rtype: :class:`CGroupInfo` | None + + """ + # Clean up the line + line = line.strip() + + # Ensure the line is valid + match = cls.LINE_RE.match(line) + if not match: + return None + + id_, groups, path = match.groups() + + # Parse the controllers from the groups + controllers = [c.strip() for c in groups.split(",") if c.strip()] + + # Break up the path to grab container_id and pod_id if available + # e.g. /docker/ + # e.g. /kubepods/test/pod/ + parts = [p for p in path.split("/")] + + # Grab the container id from the path if a valid id is present + container_id = None + if len(parts): + match = cls.CONTAINER_RE.match(parts.pop()) + if match: + container_id = match.group(1) + + # Grab the pod id from the path if a valid id is present + pod_id = None + if len(parts): + match = cls.POD_RE.match(parts.pop()) + if match: + pod_id = match.group(1) + + return cls(id=id_, groups=groups, path=path, container_id=container_id, controllers=controllers, pod_id=pod_id) + + +def get_container_info(pid="self"): + # type: (str) -> Optional[CGroupInfo] + """ + Helper to fetch the current container id, if we are running in a container + + We will parse `/proc/{pid}/cgroup` to determine our container id. + + The results of calling this function are cached + + :param pid: The pid of the cgroup file to parse (default: 'self') + :type pid: str | int + :returns: The cgroup file info if found, or else None + :rtype: :class:`CGroupInfo` | None + """ + + cgroup_file = "/proc/{0}/cgroup".format(pid) + + try: + with open(cgroup_file, mode="r") as fp: + for line in fp: + info = CGroupInfo.from_line(line) + if info and info.container_id: + return info + except IOError as e: + if e.errno != errno.ENOENT: + log.debug("Failed to open cgroup file for pid %r", pid, exc_info=True) + except Exception: + log.debug("Failed to parse cgroup file for pid %r", pid, exc_info=True) + return None diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/metric_collectors.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/metric_collectors.py new file mode 100644 index 000000000..67a9e1642 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/metric_collectors.py @@ -0,0 +1,94 @@ +import os +from typing import List +from typing import Tuple + +from .collector import ValueCollector +from .constants import CPU_PERCENT +from .constants import CPU_TIME_SYS +from .constants import CPU_TIME_USER +from .constants import CTX_SWITCH_INVOLUNTARY +from .constants import CTX_SWITCH_VOLUNTARY +from .constants import GC_COUNT_GEN0 +from .constants import GC_COUNT_GEN1 +from .constants import GC_COUNT_GEN2 +from .constants import MEM_RSS +from .constants import THREAD_COUNT + + +class RuntimeMetricCollector(ValueCollector): + value = [] # type: List[Tuple[str, str]] + periodic = True + + +class GCRuntimeMetricCollector(RuntimeMetricCollector): + """Collector for garbage collection generational counts + + More information at https://docs.python.org/3/library/gc.html + """ + + required_modules = ["gc"] + + def collect_fn(self, keys): + gc = self.modules.get("gc") + + counts = gc.get_count() + metrics = [ + (GC_COUNT_GEN0, counts[0]), + (GC_COUNT_GEN1, counts[1]), + (GC_COUNT_GEN2, counts[2]), + ] + + return metrics + + +class PSUtilRuntimeMetricCollector(RuntimeMetricCollector): + """Collector for psutil metrics. + + Performs batched operations via proc.oneshot() to optimize the calls. + See https://psutil.readthedocs.io/en/latest/#psutil.Process.oneshot + for more information. + """ + + required_modules = ["ddtrace.vendor.psutil"] + stored_value = dict( + CPU_TIME_SYS_TOTAL=0, + CPU_TIME_USER_TOTAL=0, + CTX_SWITCH_VOLUNTARY_TOTAL=0, + CTX_SWITCH_INVOLUNTARY_TOTAL=0, + ) + + def _on_modules_load(self): + self.proc = self.modules["ddtrace.vendor.psutil"].Process(os.getpid()) + + def collect_fn(self, keys): + with self.proc.oneshot(): + # only return time deltas + # TODO[tahir]: better abstraction for metrics based on last value + cpu_time_sys_total = self.proc.cpu_times().system + cpu_time_user_total = self.proc.cpu_times().user + cpu_time_sys = cpu_time_sys_total - self.stored_value["CPU_TIME_SYS_TOTAL"] + cpu_time_user = cpu_time_user_total - self.stored_value["CPU_TIME_USER_TOTAL"] + + ctx_switch_voluntary_total = self.proc.num_ctx_switches().voluntary + ctx_switch_involuntary_total = self.proc.num_ctx_switches().involuntary + ctx_switch_voluntary = ctx_switch_voluntary_total - self.stored_value["CTX_SWITCH_VOLUNTARY_TOTAL"] + ctx_switch_involuntary = ctx_switch_involuntary_total - self.stored_value["CTX_SWITCH_INVOLUNTARY_TOTAL"] + + self.stored_value = dict( + CPU_TIME_SYS_TOTAL=cpu_time_sys_total, + CPU_TIME_USER_TOTAL=cpu_time_user_total, + CTX_SWITCH_VOLUNTARY_TOTAL=ctx_switch_voluntary_total, + CTX_SWITCH_INVOLUNTARY_TOTAL=ctx_switch_involuntary_total, + ) + + metrics = [ + (THREAD_COUNT, self.proc.num_threads()), + (MEM_RSS, self.proc.memory_info().rss), + (CTX_SWITCH_VOLUNTARY, ctx_switch_voluntary), + (CTX_SWITCH_INVOLUNTARY, ctx_switch_involuntary), + (CPU_TIME_SYS, cpu_time_sys), + (CPU_TIME_USER, cpu_time_user), + (CPU_PERCENT, self.proc.cpu_percent()), + ] + + return metrics diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/runtime_metrics.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/runtime_metrics.py new file mode 100644 index 000000000..28d6c5b5a --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/runtime_metrics.py @@ -0,0 +1,165 @@ +import itertools +from typing import ClassVar +from typing import Optional +from typing import Set +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from ddtrace import Span + +import attr + +import ddtrace +from ddtrace.internal import forksafe + +from .. import periodic +from ...utils.formats import get_env +from ..dogstatsd import get_dogstatsd_client +from ..logger import get_logger +from .constants import DEFAULT_RUNTIME_METRICS +from .constants import DEFAULT_RUNTIME_TAGS +from .metric_collectors import GCRuntimeMetricCollector +from .metric_collectors import PSUtilRuntimeMetricCollector +from .tag_collectors import PlatformTagCollector +from .tag_collectors import TracerTagCollector + + +log = get_logger(__name__) + + +class RuntimeCollectorsIterable(object): + def __init__(self, enabled=None): + self._enabled = enabled or self.ENABLED + # Initialize the collectors. + self._collectors = [c() for c in self.COLLECTORS] + + def __iter__(self): + collected = (collector.collect(self._enabled) for collector in self._collectors) + return itertools.chain.from_iterable(collected) + + def __repr__(self): + return "{}(enabled={})".format( + self.__class__.__name__, + self._enabled, + ) + + +class RuntimeTags(RuntimeCollectorsIterable): + ENABLED = DEFAULT_RUNTIME_TAGS + COLLECTORS = [ + PlatformTagCollector, + TracerTagCollector, + ] + + +class RuntimeMetrics(RuntimeCollectorsIterable): + ENABLED = DEFAULT_RUNTIME_METRICS + COLLECTORS = [ + GCRuntimeMetricCollector, + PSUtilRuntimeMetricCollector, + ] + + +def _get_interval_or_default(): + return float(get_env("runtime_metrics", "interval", default=10)) + + +@attr.s(eq=False) +class RuntimeWorker(periodic.PeriodicService): + """Worker thread for collecting and writing runtime metrics to a DogStatsd + client. + """ + + _interval = attr.ib(type=float, factory=_get_interval_or_default) + tracer = attr.ib(type=ddtrace.Tracer, default=None) + dogstatsd_url = attr.ib(type=Optional[str], default=None) + _dogstatsd_client = attr.ib(init=False, repr=False) + _runtime_metrics = attr.ib(factory=RuntimeMetrics, repr=False) + _services = attr.ib(type=Set[str], init=False, factory=set) + + enabled = False + _instance = None # type: ClassVar[Optional[RuntimeWorker]] + _lock = forksafe.Lock() + + def __attrs_post_init__(self): + # type: () -> None + self._dogstatsd_client = get_dogstatsd_client(self.dogstatsd_url or ddtrace.internal.agent.get_stats_url()) + self.tracer = self.tracer or ddtrace.tracer + self.tracer.on_start_span(self._set_language_on_span) + + def _set_language_on_span( + self, + span, # type: Span + ): + # type: (...) -> None + # add tags to root span to correlate trace with runtime metrics + # only applied to spans with types that are internal to applications + if span.parent_id is None and self.tracer._is_span_internal(span): + span.meta["language"] = "python" + + @classmethod + def disable(cls): + # type: () -> None + with cls._lock: + if cls._instance is None: + return + + forksafe.unregister(cls._restart) + + cls._instance.stop() + cls._instance.join() + cls._instance = None + cls.enabled = False + + @classmethod + def _restart(cls): + cls.disable() + cls.enable() + + @classmethod + def enable(cls, flush_interval=None, tracer=None, dogstatsd_url=None): + # type: (Optional[float], Optional[ddtrace.Tracer], Optional[str]) -> None + with cls._lock: + if cls._instance is not None: + return + if flush_interval is None: + flush_interval = _get_interval_or_default() + runtime_worker = cls(flush_interval, tracer, dogstatsd_url) # type: ignore[arg-type] + runtime_worker.start() + # force an immediate update constant tags + runtime_worker.update_runtime_tags() + + forksafe.register(cls._restart) + + cls._instance = runtime_worker + cls.enabled = True + + def flush(self): + # type: () -> None + # The constant tags for the dogstatsd client needs to updated with any new + # service(s) that may have been added. + if self._services != self.tracer._services: + self._services = self.tracer._services + self.update_runtime_tags() + + with self._dogstatsd_client: + for key, value in self._runtime_metrics: + log.debug("Writing metric %s:%s", key, value) + self._dogstatsd_client.distribution(key, value) + + def _stop_service(self): # type: ignore[override] + # type: (...) -> None + # De-register span hook + super(RuntimeWorker, self)._stop_service() + self.tracer.deregister_on_start_span(self._set_language_on_span) + + def update_runtime_tags(self): + # type: () -> None + # DEV: ddstatsd expects tags in the form ['key1:value1', 'key2:value2', ...] + tags = ["{}:{}".format(k, v) for k, v in RuntimeTags()] + log.debug("Updating constant tags %s", tags) + self._dogstatsd_client.constant_tags = tags + + periodic = flush + on_shutdown = flush diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/tag_collectors.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/tag_collectors.py new file mode 100644 index 000000000..21caa7ab3 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/runtime/tag_collectors.py @@ -0,0 +1,59 @@ +from typing import List +from typing import Tuple + +from ...constants import ENV_KEY +from .collector import ValueCollector +from .constants import LANG +from .constants import LANG_INTERPRETER +from .constants import LANG_VERSION +from .constants import SERVICE +from .constants import TRACER_VERSION + + +class RuntimeTagCollector(ValueCollector): + periodic = False + value = [] # type: List[Tuple[str, str]] + + +class TracerTagCollector(RuntimeTagCollector): + """Tag collector for the ddtrace Tracer""" + + required_modules = ["ddtrace"] + + def collect_fn(self, keys): + ddtrace = self.modules.get("ddtrace") + # make sure to copy _services to avoid RuntimeError: Set changed size during iteration + tags = [(SERVICE, service) for service in list(ddtrace.tracer._services)] + if ENV_KEY in ddtrace.tracer.tags: + tags.append((ENV_KEY, ddtrace.tracer.tags[ENV_KEY])) + return tags + + +class PlatformTagCollector(RuntimeTagCollector): + """Tag collector for the Python interpreter implementation. + + Tags collected: + - ``lang_interpreter``: + + * For CPython this is 'CPython'. + * For Pypy this is ``PyPy`` + * For Jython this is ``Jython`` + + - `lang_version``, eg ``2.7.10`` + - ``lang`` e.g. ``Python`` + - ``tracer_version`` e.g. ``0.29.0`` + + """ + + required_modules = ["platform", "ddtrace"] + + def collect_fn(self, keys): + platform = self.modules.get("platform") + ddtrace = self.modules.get("ddtrace") + tags = [ + (LANG, "python"), + (LANG_INTERPRETER, platform.python_implementation()), + (LANG_VERSION, platform.python_version()), + (TRACER_VERSION, ddtrace.__version__), + ] + return tags diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/service.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/service.py new file mode 100644 index 000000000..38bdc5e02 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/service.py @@ -0,0 +1,106 @@ +import abc +import enum +import typing + +import attr +import six + +from . import forksafe + + +class ServiceStatus(enum.Enum): + """A Service status.""" + + STOPPED = "stopped" + RUNNING = "running" + + +class ServiceStatusError(RuntimeError): + def __init__( + self, + service_cls, # type: typing.Type[Service] + current_status, # type: ServiceStatus + ): + # type: (...) -> None + self.current_status = current_status + super(ServiceStatusError, self).__init__( + "%s is already in status %s" % (service_cls.__name__, current_status.value) + ) + + +@attr.s(eq=False) +class Service(six.with_metaclass(abc.ABCMeta)): + """A service that can be started or stopped.""" + + status = attr.ib(default=ServiceStatus.STOPPED, type=ServiceStatus, init=False, eq=False) + _service_lock = attr.ib(factory=forksafe.Lock, repr=False, init=False, eq=False) + + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.stop() + self.join() + + def start( + self, + *args, # type: typing.Any + **kwargs # type: typing.Any + ): + # type: (...) -> None + """Start the service.""" + # Use a lock so we're sure that if 2 threads try to start the service at the same time, one of them will raise + # an error. + with self._service_lock: + if self.status == ServiceStatus.RUNNING: + raise ServiceStatusError(self.__class__, self.status) + self._start_service(*args, **kwargs) + self.status = ServiceStatus.RUNNING + + @abc.abstractmethod + def _start_service( + self, + *args, # type: typing.Any + **kwargs # type: typing.Any + ): + # type: (...) -> None + """Start the service for real. + + This method uses the internal lock to be sure there's no race conditions and that the service is really started + once start() returns. + + """ + + def stop( + self, + *args, # type: typing.Any + **kwargs # type: typing.Any + ): + # type: (...) -> None + """Stop the service.""" + with self._service_lock: + if self.status == ServiceStatus.STOPPED: + raise ServiceStatusError(self.__class__, self.status) + self._stop_service(*args, **kwargs) + self.status = ServiceStatus.STOPPED + + @abc.abstractmethod + def _stop_service( + self, + *args, # type: typing.Any + **kwargs # type: typing.Any + ): + # type: (...) -> None + """Stop the service for real. + + This method uses the internal lock to be sure there's no race conditions and that the service is really stopped + once start() returns. + + """ + + def join( + self, timeout=None # type: typing.Optional[float] + ): + # type: (...) -> None + """Join the service once stopped.""" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/sma.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/sma.py new file mode 100644 index 000000000..a4538847d --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/sma.py @@ -0,0 +1,67 @@ +from __future__ import division + + +class SimpleMovingAverage(object): + """ + Simple Moving Average implementation. + """ + + __slots__ = ( + "size", + "index", + "counts", + "totals", + "sum_count", + "sum_total", + ) + + def __init__(self, size): + # type: (int) -> None + """ + Constructor for SimpleMovingAverage. + + :param size: The size of the window to calculate the moving average. + :type size: :obj:`int` + """ + if size < 1: + raise ValueError + + self.index = 0 + self.size = size + + self.sum_count = 0 + self.sum_total = 0 + + self.counts = [0] * self.size + self.totals = [0] * self.size + + def get(self): + # type: () -> float + """ + Get the current SMA value. + """ + if self.sum_total == 0: + return 0.0 + + return float(self.sum_count) / self.sum_total + + def set(self, count, total): + # type: (int, int) -> None + """ + Set the value of the next bucket and update the SMA value. + + :param count: The valid quantity of the next bucket. + :type count: :obj:`int` + :param total: The total quantity of the next bucket. + :type total: :obj:`int` + """ + if count > total: + raise ValueError + + self.sum_count += count - self.counts[self.index] + self.sum_total += total - self.totals[self.index] + + self.counts[self.index] = count + self.totals[self.index] = total + + self.index = (self.index + 1) % self.size diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/uds.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/uds.py new file mode 100644 index 000000000..41ca57aa9 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/uds.py @@ -0,0 +1,27 @@ +import socket +from typing import Any + +from .compat import httplib +from .http import BasePathMixin + + +class UDSHTTPConnection(BasePathMixin, httplib.HTTPConnection): + """An HTTP connection established over a Unix Domain Socket.""" + + # It's "important" to keep the hostname and port arguments here; while there are not used by the connection + # mechanism, they are actually used as HTTP headers such as `Host`. + def __init__( + self, + path, # type: str + *args, # type: Any + **kwargs # type: Any + ): + # type: (...) -> None + super(UDSHTTPConnection, self).__init__(*args, **kwargs) + self.path = path + + def connect(self): + # type: () -> None + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(self.path) + self.sock = sock diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/uwsgi.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/uwsgi.py new file mode 100644 index 000000000..065806210 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/uwsgi.py @@ -0,0 +1,70 @@ +from __future__ import absolute_import + +from typing import Callable +from typing import Optional + + +class uWSGIConfigError(Exception): + """uWSGI configuration error. + + This is raised when uwsgi configuration is incompatible with the library. + """ + + +class uWSGIMasterProcess(Exception): + """The process is uWSGI master process.""" + + +def check_uwsgi(worker_callback=None, atexit=None): + # type: (Optional[Callable], Optional[Callable]) -> None + """Check whetever uwsgi is running and what needs to be done. + + :param worker_callback: Callback function to call in uWSGI worker processes. + """ + try: + import uwsgi + except ImportError: + return + + if not uwsgi.opt.get("enable-threads"): + raise uWSGIConfigError("enable-threads option must be set to true") + + # If uwsgi has more than one process, it is running in prefork operational mode: uwsgi is going to fork multiple + # sub-processes. + # If lazy-app is enabled, then the app is loaded in each subprocess independently. This is fine. + # If it's not enabled, then the app will be loaded in the master process, and uwsgi will `fork()` abruptly, + # bypassing Python sanity checks. We need to handle this case properly. + # The proper way to handle that is to allow to register a callback function to run in the subprocess at their + # startup, and warn the caller that this is the master process and that (probably) nothing should be done. + if uwsgi.numproc > 1 and not uwsgi.opt.get("lazy-apps") and uwsgi.worker_id() == 0: + + if not uwsgi.opt.get("master"): + # Having multiple workers without the master process is not supported: + # the postfork hooks are not available, so there's no way to start a different profiler in each + # worker + raise uWSGIConfigError("master option must be enabled when multiple processes are used") + + # Register the function to be called in child process at startup + if worker_callback is not None: + try: + import uwsgidecorators + except ImportError: + raise uWSGIConfigError("Running under uwsgi but uwsgidecorators cannot be imported") + uwsgidecorators.postfork(worker_callback) + + if atexit is not None: + + original_atexit = getattr(uwsgi, "atexit", None) + + def _atexit(): + try: + atexit() + except Exception: + pass + + if original_atexit is not None: + original_atexit() + + uwsgi.atexit = _atexit + + raise uWSGIMasterProcess() diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/writer.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/writer.py new file mode 100644 index 000000000..9c11e1990 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/internal/writer.py @@ -0,0 +1,493 @@ +import abc +from collections import defaultdict +from json import loads +import logging +import os +import sys +from typing import List +from typing import Optional +from typing import TYPE_CHECKING +from typing import TextIO + +import six +import tenacity + +import ddtrace +from ddtrace.vendor.dogstatsd import DogStatsd + +from . import agent +from . import compat +from . import periodic +from . import service +from ..constants import KEEP_SPANS_RATE_KEY +from ..sampler import BasePrioritySampler +from ..sampler import BaseSampler +from ..utils.formats import get_env +from ..utils.formats import parse_tags_str +from ..utils.time import StopWatch +from ._encoding import BufferFull +from ._encoding import BufferItemTooLarge +from .agent import get_connection +from .encoding import Encoder +from .encoding import JSONEncoderV2 +from .logger import get_logger +from .runtime import container +from .sma import SimpleMovingAverage + + +if TYPE_CHECKING: + from ddtrace import Span + + +log = get_logger(__name__) + +LOG_ERR_INTERVAL = 60 + +# The window size should be chosen so that the look-back period is +# greater-equal to the agent API's timeout. Although most tracers have a +# 2s timeout, the java tracer has a 10s timeout, so we set the window size +# to 10 buckets of 1s duration. +DEFAULT_SMA_WINDOW = 10 + +DEFAULT_BUFFER_SIZE = 8 << 20 # 8 MB +DEFAULT_MAX_PAYLOAD_SIZE = 8 << 20 # 8 MB +DEFAULT_PROCESSING_INTERVAL = 1.0 + + +def get_writer_buffer_size(): + # type: () -> int + return int(get_env("trace", "writer_buffer_size_bytes", default=DEFAULT_BUFFER_SIZE)) # type: ignore[arg-type] + + +def get_writer_max_payload_size(): + # type: () -> int + return int( + get_env("trace", "writer_max_payload_size_bytes", default=DEFAULT_MAX_PAYLOAD_SIZE) # type: ignore[arg-type] + ) + + +def get_writer_interval_seconds(): + # type: () -> float + return float( + get_env("trace", "writer_interval_seconds", default=DEFAULT_PROCESSING_INTERVAL) # type: ignore[arg-type] + ) + + +def _human_size(nbytes): + """Return a human-readable size.""" + i = 0 + suffixes = ["B", "KB", "MB", "GB", "TB"] + while nbytes >= 1000 and i < len(suffixes) - 1: + nbytes /= 1000.0 + i += 1 + f = ("%.2f" % nbytes).rstrip("0").rstrip(".") + return "%s%s" % (f, suffixes[i]) + + +class Response(object): + """ + Custom API Response object to represent a response from calling the API. + + We do this to ensure we know expected properties will exist, and so we + can call `resp.read()` and load the body once into an instance before we + close the HTTPConnection used for the request. + """ + + __slots__ = ["status", "body", "reason", "msg"] + + def __init__(self, status=None, body=None, reason=None, msg=None): + self.status = status + self.body = body + self.reason = reason + self.msg = msg + + @classmethod + def from_http_response(cls, resp): + """ + Build a ``Response`` from the provided ``HTTPResponse`` object. + + This function will call `.read()` to consume the body of the ``HTTPResponse`` object. + + :param resp: ``HTTPResponse`` object to build the ``Response`` from + :type resp: ``HTTPResponse`` + :rtype: ``Response`` + :returns: A new ``Response`` + """ + return cls( + status=resp.status, + body=resp.read(), + reason=getattr(resp, "reason", None), + msg=getattr(resp, "msg", None), + ) + + def get_json(self): + """Helper to parse the body of this request as JSON""" + try: + body = self.body + if not body: + log.debug("Empty reply from Datadog Agent, %r", self) + return + + if not isinstance(body, str) and hasattr(body, "decode"): + body = body.decode("utf-8") + + if hasattr(body, "startswith") and body.startswith("OK"): + # This typically happens when using a priority-sampling enabled + # library with an outdated agent. It still works, but priority sampling + # will probably send too many traces, so the next step is to upgrade agent. + log.debug( + "Cannot parse Datadog Agent response. " + "This occurs because Datadog agent is out of date or DATADOG_PRIORITY_SAMPLING=false is set" + ) + return + + return loads(body) + except (ValueError, TypeError): + log.debug("Unable to parse Datadog Agent JSON response: %r", body, exc_info=True) + + def __repr__(self): + return "{0}(status={1!r}, body={2!r}, reason={3!r}, msg={4!r})".format( + self.__class__.__name__, + self.status, + self.body, + self.reason, + self.msg, + ) + + +class TraceWriter(six.with_metaclass(abc.ABCMeta)): + @abc.abstractmethod + def recreate(self): + # type: () -> TraceWriter + pass + + @abc.abstractmethod + def stop(self, timeout=None): + # type: (Optional[float]) -> None + pass + + @abc.abstractmethod + def write(self, spans=None): + # type: (Optional[List[Span]]) -> None + pass + + +class LogWriter(TraceWriter): + def __init__( + self, + out=sys.stdout, # type: TextIO + sampler=None, # type: Optional[BaseSampler] + priority_sampler=None, # type: Optional[BasePrioritySampler] + ): + # type: (...) -> None + self._sampler = sampler + self._priority_sampler = priority_sampler + self.encoder = JSONEncoderV2() + self.out = out + + def recreate(self): + # type: () -> LogWriter + """Create a new instance of :class:`LogWriter` using the same settings from this instance + + :rtype: :class:`LogWriter` + :returns: A new :class:`LogWriter` instance + """ + writer = self.__class__(out=self.out, sampler=self._sampler, priority_sampler=self._priority_sampler) + return writer + + def stop(self, timeout=None): + # type: (Optional[float]) -> None + return + + def write(self, spans=None): + # type: (Optional[List[Span]]) -> None + if not spans: + return + + encoded = self.encoder.encode_traces([spans]) + self.out.write(encoded + "\n") + self.out.flush() + + +class AgentWriter(periodic.PeriodicService, TraceWriter): + """Writer to the Datadog Agent. + + The Datadog Agent supports (at the time of writing this) receiving trace + payloads up to 50MB. A trace payload is just a list of traces and the agent + expects a trace to be complete. That is, all spans with the same trace_id + should be in the same trace. + """ + + RETRY_ATTEMPTS = 3 + + def __init__( + self, + agent_url, # type: str + sampler=None, # type: Optional[BaseSampler] + priority_sampler=None, # type: Optional[BasePrioritySampler] + processing_interval=get_writer_interval_seconds(), # type: float + # Match the payload size since there is no functionality + # to flush dynamically. + buffer_size=get_writer_buffer_size(), # type: int + max_payload_size=get_writer_max_payload_size(), # type: int + timeout=agent.get_trace_agent_timeout(), # type: float + dogstatsd=None, # type: Optional[DogStatsd] + report_metrics=False, # type: bool + sync_mode=False, # type: bool + ): + # type: (...) -> None + super(AgentWriter, self).__init__(interval=processing_interval) + self.agent_url = agent_url + self._buffer_size = buffer_size + self._max_payload_size = max_payload_size + self._sampler = sampler + self._priority_sampler = priority_sampler + self._headers = { + "Datadog-Meta-Lang": "python", + "Datadog-Meta-Lang-Version": compat.PYTHON_VERSION, + "Datadog-Meta-Lang-Interpreter": compat.PYTHON_INTERPRETER, + "Datadog-Meta-Tracer-Version": ddtrace.__version__, + } + self._timeout = timeout + + if priority_sampler is not None: + self._endpoint = "v0.4/traces" + else: + self._endpoint = "v0.3/traces" + + self._container_info = container.get_container_info() + if self._container_info and self._container_info.container_id: + self._headers.update( + { + "Datadog-Container-Id": self._container_info.container_id, + } + ) + + self._encoder = Encoder( + max_size=self._buffer_size, + max_item_size=self._max_payload_size, + ) + self._headers.update({"Content-Type": self._encoder.content_type}) + additional_header_str = os.environ.get("_DD_TRACE_WRITER_ADDITIONAL_HEADERS") + if additional_header_str is not None: + self._headers.update(parse_tags_str(additional_header_str)) + self.dogstatsd = dogstatsd + self._report_metrics = report_metrics + self._metrics_reset() + self._drop_sma = SimpleMovingAverage(DEFAULT_SMA_WINDOW) + self._sync_mode = sync_mode + self._retry_upload = tenacity.Retrying( + # Retry RETRY_ATTEMPTS times within the first half of the processing + # interval, using a Fibonacci policy with jitter + wait=tenacity.wait_random_exponential( + multiplier=0.618 * self.interval / (1.618 ** self.RETRY_ATTEMPTS) / 2, exp_base=1.618 + ), + stop=tenacity.stop_after_attempt(self.RETRY_ATTEMPTS), + retry=tenacity.retry_if_exception_type((compat.httplib.HTTPException, OSError, IOError)), + ) + + def _metrics_dist(self, name, count=1, tags=None): + self._metrics[name]["count"] += count + if tags: + self._metrics[name]["tags"].extend(tags) + + def _metrics_reset(self): + self._metrics = defaultdict(lambda: {"count": 0, "tags": []}) + + def _set_drop_rate(self): + dropped = sum( + self._metrics[metric]["count"] + for metric in ("encoder.dropped.traces", "buffer.dropped.traces", "http.dropped.traces") + ) + accepted = self._metrics["writer.accepted.traces"]["count"] + + if dropped > accepted: + # Sanity check, we cannot drop more traces than we accepted. + log.error("dropped more traces than accepted (dropped: %d, accepted: %d)", dropped, accepted) + + accepted = dropped + + self._drop_sma.set(dropped, accepted) + + def _set_keep_rate(self, trace): + if trace: + trace[0].set_metric(KEEP_SPANS_RATE_KEY, 1.0 - self._drop_sma.get()) + + def recreate(self): + # type: () -> AgentWriter + writer = self.__class__( + agent_url=self.agent_url, + priority_sampler=self._priority_sampler, + sync_mode=self._sync_mode, + ) + writer._headers = self._headers + writer._endpoint = self._endpoint + return writer + + def _put(self, data, headers): + conn = get_connection(self.agent_url, self._timeout) + + with StopWatch() as sw: + try: + conn.request("PUT", self._endpoint, data, headers) + resp = compat.get_connection_response(conn) + t = sw.elapsed() + if t >= self.interval: + log_level = logging.WARNING + else: + log_level = logging.DEBUG + log.log(log_level, "sent %s in %.5fs to %s", _human_size(len(data)), t, self.agent_url) + return Response.from_http_response(resp) + finally: + conn.close() + + def _downgrade(self, payload, response): + if self._endpoint == "v0.4/traces": + self._endpoint = "v0.3/traces" + return payload + raise ValueError + + def _send_payload(self, payload, count): + headers = self._headers.copy() + headers["X-Datadog-Trace-Count"] = str(count) + + self._metrics_dist("http.requests") + + response = self._put(payload, headers) + + if response.status >= 400: + self._metrics_dist("http.errors", tags=["type:%s" % response.status]) + else: + self._metrics_dist("http.sent.bytes", len(payload)) + + if response.status in [404, 415]: + log.debug("calling endpoint '%s' but received %s; downgrading API", self._endpoint, response.status) + try: + payload = self._downgrade(payload, response) + except ValueError: + log.error( + "unsupported endpoint '%s': received response %s from Datadog Agent (%s)", + self._endpoint, + response.status, + self.agent_url, + ) + else: + return self._send_payload(payload, count) + elif response.status >= 400: + log.error( + "failed to send traces to Datadog Agent at %s: HTTP error status %s, reason %s", + self.agent_url, + response.status, + response.reason, + ) + self._metrics_dist("http.dropped.bytes", len(payload)) + self._metrics_dist("http.dropped.traces", count) + elif self._priority_sampler or isinstance(self._sampler, BasePrioritySampler): + result_traces_json = response.get_json() + if result_traces_json and "rate_by_service" in result_traces_json: + try: + if self._priority_sampler: + self._priority_sampler.update_rate_by_service_sample_rates( + result_traces_json["rate_by_service"], + ) + if isinstance(self._sampler, BasePrioritySampler): + self._sampler.update_rate_by_service_sample_rates( + result_traces_json["rate_by_service"], + ) + except ValueError: + log.error("sample_rate is negative, cannot update the rate samplers") + + def write(self, spans=None): + # type: (Optional[List[Span]]) -> None + if spans is None: + return + + if self._sync_mode is False: + # Start the AgentWriter on first write. + try: + if self.status != service.ServiceStatus.RUNNING: + self.start() + except service.ServiceStatusError: + pass + + self._metrics_dist("writer.accepted.traces") + self._set_keep_rate(spans) + + try: + self._encoder.put(spans) + except BufferItemTooLarge as e: + payload_size = e.args[0] + log.warning( + "trace (%db) larger than payload buffer limit (%db), dropping", + payload_size, + self._buffer_size, + ) + self._metrics_dist("buffer.dropped.traces", 1, tags=["reason:t_too_big"]) + self._metrics_dist("buffer.dropped.bytes", payload_size, tags=["reason:t_too_big"]) + except BufferFull as e: + payload_size = e.args[0] + log.warning( + "trace buffer (%s traces %db/%db) cannot fit trace of size %db, dropping", + len(self._encoder), + self._encoder.size, + self._encoder.max_size, + payload_size, + ) + self._metrics_dist("buffer.dropped.traces", 1, tags=["reason:full"]) + self._metrics_dist("buffer.dropped.bytes", payload_size, tags=["reason:full"]) + else: + self._metrics_dist("buffer.accepted.traces", 1) + self._metrics_dist("buffer.accepted.spans", len(spans)) + if self._sync_mode: + self.flush_queue() + + def flush_queue(self, raise_exc=False): + # type: (bool) -> None + try: + try: + n_traces = len(self._encoder) + encoded = self._encoder.encode() + if encoded is None: + return + except Exception: + log.error("failed to encode trace with encoder %r", self._encoder, exc_info=True) + self._metrics_dist("encoder.dropped.traces", n_traces) + return + + try: + self._retry_upload(self._send_payload, encoded, n_traces) + except tenacity.RetryError as e: + self._metrics_dist("http.errors", tags=["type:err"]) + self._metrics_dist("http.dropped.bytes", len(encoded)) + self._metrics_dist("http.dropped.traces", n_traces) + if raise_exc: + e.reraise() + else: + log.error("failed to send traces to Datadog Agent at %s", self.agent_url, exc_info=True) + finally: + if self._report_metrics and self.dogstatsd: + # Note that we cannot use the batching functionality of dogstatsd because + # it's not thread-safe. + # https://github.com/DataDog/datadogpy/issues/439 + # This really isn't ideal as now we're going to do a ton of socket calls. + self.dogstatsd.distribution("datadog.tracer.http.sent.bytes", len(encoded)) + self.dogstatsd.distribution("datadog.tracer.http.sent.traces", n_traces) + for name, metric in self._metrics.items(): + self.dogstatsd.distribution("datadog.tracer.%s" % name, metric["count"], tags=metric["tags"]) + finally: + self._set_drop_rate() + self._metrics_reset() + + def periodic(self): + self.flush_queue(raise_exc=False) + + def _stop_service( # type: ignore[override] + self, + timeout=None, # type: Optional[float] + ): + # type: (...) -> None + # FIXME: don't join() on stop(), let the caller handle this + super(AgentWriter, self)._stop_service() + self.join(timeout=timeout) + + on_shutdown = periodic diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/monkey.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/monkey.py new file mode 100644 index 000000000..316c0e2b8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/monkey.py @@ -0,0 +1,281 @@ +"""Patch libraries to be automatically instrumented. + +It can monkey patch supported standard libraries and third party modules. +A patched module will automatically report spans with its default configuration. + +A library instrumentation can be configured (for instance, to report as another service) +using Pin. For that, check its documentation. +""" +import importlib +import os +import sys +import threading +from typing import Any +from typing import Callable +from typing import List + +from ddtrace.vendor.wrapt.importer import when_imported + +from .internal.logger import get_logger +from .settings import _config as config +from .utils import formats +from .utils.deprecation import deprecated + + +log = get_logger(__name__) + +# Default set of modules to automatically patch or not +PATCH_MODULES = { + "asyncio": True, + "boto": True, + "botocore": True, + "bottle": False, + "cassandra": True, + "celery": True, + "consul": True, + "django": True, + "elasticsearch": True, + "algoliasearch": True, + "futures": True, + "grpc": True, + "httpx": True, + "mongoengine": True, + "mysql": True, + "mysqldb": True, + "pymysql": True, + "mariadb": True, + "psycopg": True, + "pylibmc": True, + "pymemcache": True, + "pymongo": True, + "redis": True, + "rediscluster": True, + "requests": True, + "rq": True, + "sanic": True, + "snowflake": False, + "sqlalchemy": False, # Prefer DB client instrumentation + "sqlite3": True, + "aiohttp": True, # requires asyncio (Python 3.4+) + "aiopg": True, + "aiobotocore": False, + "httplib": False, + "urllib3": False, + "vertica": True, + "molten": True, + "jinja2": True, + "mako": True, + "flask": True, + "kombu": False, + "starlette": True, + # Ignore some web framework integrations that might be configured explicitly in code + "falcon": False, + "pylons": False, + "pyramid": False, + # Auto-enable logging if the environment variable DD_LOGS_INJECTION is true + "logging": config.logs_injection, + "pynamodb": True, + "pyodbc": True, + "fastapi": True, + "dogpile_cache": True, +} + +_LOCK = threading.Lock() +_PATCHED_MODULES = set() + +# Modules which are patched on first use +# DEV: These modules are patched when the user first imports them, rather than +# explicitly importing and patching them on application startup `ddtrace.patch_all(module=True)` +# DEV: This ensures we do not patch a module until it is needed +# DEV: => +_PATCH_ON_IMPORT = { + "aiohttp": ("aiohttp",), + "aiobotocore": ("aiobotocore",), + "celery": ("celery",), + "flask": ("flask",), + "gevent": ("gevent",), + "requests": ("requests",), + "botocore": ("botocore",), + "elasticsearch": ( + "elasticsearch", + "elasticsearch2", + "elasticsearch5", + "elasticsearch6", + "elasticsearch7", + ), + "pynamodb": ("pynamodb",), +} + + +class PatchException(Exception): + """Wraps regular `Exception` class when patching modules""" + + pass + + +class ModuleNotFoundException(PatchException): + pass + + +def _on_import_factory(module, raise_errors=True): + # type: (str, bool) -> Callable[[Any], None] + """Factory to create an import hook for the provided module name""" + + def on_import(hook): + # Import and patch module + path = "ddtrace.contrib.%s" % module + try: + imported_module = importlib.import_module(path) + except ImportError: + if raise_errors: + raise + log.error("failed to import ddtrace module %r when patching on import", path, exc_info=True) + else: + imported_module.patch() + + return on_import + + +def patch_all(**patch_modules): + # type: (bool) -> None + """Automatically patches all available modules. + + In addition to ``patch_modules``, an override can be specified via an + environment variable, ``DD_TRACE__ENABLED`` for each module. + + ``patch_modules`` have the highest precedence for overriding. + + :param dict patch_modules: Override whether particular modules are patched or not. + + >>> patch_all(redis=False, cassandra=False) + """ + modules = PATCH_MODULES.copy() + + # The enabled setting can be overridden by environment variables + for module, enabled in modules.items(): + env_var = "DD_TRACE_%s_ENABLED" % module.upper() + if env_var not in os.environ: + continue + + override_enabled = formats.asbool(os.environ[env_var]) + modules[module] = override_enabled + + # Arguments take precedence over the environment and the defaults. + modules.update(patch_modules) + + patch(raise_errors=False, **modules) + + +def patch(raise_errors=True, **patch_modules): + # type: (bool, bool) -> None + """Patch only a set of given modules. + + :param bool raise_errors: Raise error if one patch fail. + :param dict patch_modules: List of modules to patch. + + >>> patch(psycopg=True, elasticsearch=True) + """ + modules = [m for (m, should_patch) in patch_modules.items() if should_patch] + for module in modules: + if module in _PATCH_ON_IMPORT: + modules_to_poi = _PATCH_ON_IMPORT[module] + for m in modules_to_poi: + # If the module has already been imported then patch immediately + if m in sys.modules: + _patch_module(module, raise_errors=raise_errors) + break + # Otherwise, add a hook to patch when it is imported for the first time + else: + # Use factory to create handler to close over `module` and `raise_errors` values from this loop + when_imported(m)(_on_import_factory(module, raise_errors)) + + # manually add module to patched modules + with _LOCK: + _PATCHED_MODULES.add(module) + else: + _patch_module(module, raise_errors=raise_errors) + + patched_modules = _get_patched_modules() + log.info( + "patched %s/%s modules (%s)", + len(patched_modules), + len(modules), + ",".join(patched_modules), + ) + + +@deprecated( + message="This function will be removed.", + version="1.0.0", +) +def patch_module(module, raise_errors=True): + # type: (str, bool) -> bool + return _patch_module(module, raise_errors=raise_errors) + + +def _patch_module(module, raise_errors=True): + # type: (str, bool) -> bool + """Patch a single module + + Returns if the module got properly patched. + """ + try: + return _attempt_patch_module(module) + except ModuleNotFoundException: + if raise_errors: + raise + return False + except Exception: + if raise_errors: + raise + log.debug("failed to patch %s", module, exc_info=True) + return False + + +@deprecated( + message="This function will be removed.", + version="1.0.0", +) +def get_patched_modules(): + # type: () -> List[str] + return _get_patched_modules() + + +def _get_patched_modules(): + # type: () -> List[str] + """Get the list of patched modules""" + with _LOCK: + return sorted(_PATCHED_MODULES) + + +def _attempt_patch_module(module): + # type: (str) -> bool + """_patch_module will attempt to monkey patch the module. + + Returns if the module got patched. + Can also raise errors if it fails. + """ + path = "ddtrace.contrib.%s" % module + with _LOCK: + if module in _PATCHED_MODULES and module not in _PATCH_ON_IMPORT: + log.debug("already patched: %s", path) + return False + + try: + imported_module = importlib.import_module(path) + except ImportError: + # if the import fails, the integration is not available + raise ModuleNotFoundException( + "integration module %s does not exist, module will not have tracing available" % path + ) + else: + # if patch() is not available in the module, it means + # that the library is not installed in the environment + if not hasattr(imported_module, "patch"): + raise AttributeError( + "%s.patch is not found. '%s' is not configured for this environment" % (path, module) + ) + + imported_module.patch() # type: ignore + _PATCHED_MODULES.add(module) + return True diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__init__.py new file mode 100644 index 000000000..9df526fc9 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__init__.py @@ -0,0 +1,8 @@ +from .helpers import set_global_tracer +from .tracer import Tracer + + +__all__ = [ + "Tracer", + "set_global_tracer", +] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..0e8e64d10 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/helpers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/helpers.cpython-38.pyc new file mode 100644 index 000000000..355b85a88 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/helpers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/settings.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/settings.cpython-38.pyc new file mode 100644 index 000000000..06e5311be Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/settings.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/span.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/span.cpython-38.pyc new file mode 100644 index 000000000..b3d167817 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/span.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/span_context.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/span_context.cpython-38.pyc new file mode 100644 index 000000000..5a422d987 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/span_context.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/tags.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/tags.cpython-38.pyc new file mode 100644 index 000000000..8855a9023 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/tags.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/tracer.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/tracer.cpython-38.pyc new file mode 100644 index 000000000..ba43ce57d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/tracer.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/utils.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/utils.cpython-38.pyc new file mode 100644 index 000000000..753272bca Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/__pycache__/utils.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/helpers.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/helpers.py new file mode 100644 index 000000000..c3c4c060c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/helpers.py @@ -0,0 +1,25 @@ +from typing import TYPE_CHECKING + +import opentracing + +import ddtrace + + +if TYPE_CHECKING: + from ddtrace.opentracer import Tracer + + +""" +Helper routines for Datadog OpenTracing. +""" + + +def set_global_tracer(tracer): + # type: (Tracer) -> None + """Sets the global tracers to the given tracer.""" + + # overwrite the opentracer reference + opentracing.tracer = tracer + + # overwrite the Datadog tracer reference + ddtrace.tracer = tracer._dd_tracer diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__init__.py new file mode 100644 index 000000000..04ddde701 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__init__.py @@ -0,0 +1,6 @@ +from .http import HTTPPropagator + + +__all__ = [ + "HTTPPropagator", +] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..1512946e3 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__pycache__/binary.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__pycache__/binary.cpython-38.pyc new file mode 100644 index 000000000..be63d25de Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__pycache__/binary.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__pycache__/http.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__pycache__/http.cpython-38.pyc new file mode 100644 index 000000000..a8f506ed6 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__pycache__/http.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__pycache__/propagator.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__pycache__/propagator.cpython-38.pyc new file mode 100644 index 000000000..cb4c8582b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__pycache__/propagator.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__pycache__/text.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__pycache__/text.cpython-38.pyc new file mode 100644 index 000000000..efc6c3076 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/__pycache__/text.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/binary.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/binary.py new file mode 100644 index 000000000..e69de29bb diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/http.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/http.py new file mode 100644 index 000000000..e504b3809 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/http.py @@ -0,0 +1,82 @@ +from typing import Dict + +from opentracing import InvalidCarrierException +from opentracing import SpanContextCorruptedException + +from ddtrace.propagation.http import HTTPPropagator as DDHTTPPropagator + +from ...internal.logger import get_logger +from ..span_context import SpanContext +from .propagator import Propagator + + +log = get_logger(__name__) + +HTTP_BAGGAGE_PREFIX = "ot-baggage-" +HTTP_BAGGAGE_PREFIX_LEN = len(HTTP_BAGGAGE_PREFIX) + + +class HTTPPropagator(Propagator): + """OpenTracing compatible HTTP_HEADER and TEXT_MAP format propagator. + + `HTTPPropagator` provides compatibility by using existing OpenTracing + compatible methods from the ddtracer along with new logic supporting the + outstanding OpenTracing-defined functionality. + """ + + @staticmethod + def inject(span_context, carrier): + # type: (SpanContext, Dict[str, str]) -> None + """Inject a span context into a carrier. + + *span_context* is injected into the carrier by first using an + :class:`ddtrace.propagation.http.HTTPPropagator` to inject the ddtracer + specific fields. + + Then the baggage is injected into *carrier*. + + :param span_context: span context to inject. + + :param carrier: carrier to inject into. + """ + if not isinstance(carrier, dict): + raise InvalidCarrierException("propagator expects carrier to be a dict") + + DDHTTPPropagator.inject(span_context._dd_context, carrier) + + # Add the baggage + if span_context.baggage is not None: + for key in span_context.baggage: + carrier[HTTP_BAGGAGE_PREFIX + key] = span_context.baggage[key] + + @staticmethod + def extract(carrier): + # type: (Dict[str, str]) -> SpanContext + """Extract a span context from a carrier. + + :class:`ddtrace.propagation.http.HTTPPropagator` is used to extract + ddtracer supported fields into a `ddtrace.Context` context which is + combined with new logic to extract the baggage which is returned in an + OpenTracing compatible span context. + + :param carrier: carrier to extract from. + + :return: extracted span context. + """ + if not isinstance(carrier, dict): + raise InvalidCarrierException("propagator expects carrier to be a dict") + + ddspan_ctx = DDHTTPPropagator.extract(carrier) + + # if the dd propagator fails then it will return a new empty span + # context (with trace_id=None), we however want to raise an exception + # if this occurs. + if not ddspan_ctx.trace_id: + raise SpanContextCorruptedException("failed to extract span context") + + baggage = {} + for key in carrier: + if key.startswith(HTTP_BAGGAGE_PREFIX): + baggage[key[HTTP_BAGGAGE_PREFIX_LEN:]] = carrier[key] + + return SpanContext(ddcontext=ddspan_ctx, baggage=baggage) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/propagator.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/propagator.py new file mode 100644 index 000000000..7b71268ea --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/propagator.py @@ -0,0 +1,15 @@ +import abc + +import six + + +class Propagator(six.with_metaclass(abc.ABCMeta)): + @staticmethod + @abc.abstractmethod + def inject(span_context, carrier): + pass + + @staticmethod + @abc.abstractmethod + def extract(carrier): + pass diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/text.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/propagation/text.py new file mode 100644 index 000000000..e69de29bb diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/settings.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/settings.py new file mode 100644 index 000000000..0ea853407 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/settings.py @@ -0,0 +1,41 @@ +from collections import namedtuple +from typing import Any +from typing import Dict +from typing import List + + +# Keys used for the configuration dict +ConfigKeyNames = namedtuple( + "ConfigKeyNames", + [ + "AGENT_HOSTNAME", + "AGENT_HTTPS", + "AGENT_PORT", + "DEBUG", + "ENABLED", + "GLOBAL_TAGS", + "SAMPLER", + "PRIORITY_SAMPLING", + "UDS_PATH", + "SETTINGS", + ], +) + +ConfigKeys = ConfigKeyNames( + AGENT_HOSTNAME="agent_hostname", + AGENT_HTTPS="agent_https", + AGENT_PORT="agent_port", + DEBUG="debug", + ENABLED="enabled", + GLOBAL_TAGS="global_tags", + SAMPLER="sampler", + PRIORITY_SAMPLING="priority_sampling", + UDS_PATH="uds_path", + SETTINGS="settings", +) + + +def config_invalid_keys(config): + # type: (Dict[str, Any]) -> List[str] + """Returns a list of keys that exist in *config* and not in KEYS.""" + return [key for key in config.keys() if key not in ConfigKeys] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/span.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/span.py new file mode 100644 index 000000000..6350e489b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/span.py @@ -0,0 +1,194 @@ +import threading +from typing import Any +from typing import Dict +from typing import Optional +from typing import TYPE_CHECKING +from typing import Text +from typing import Union + +from opentracing import Span as OpenTracingSpan +from opentracing.ext import tags as OTTags + +from ddtrace.constants import ERROR_MSG +from ddtrace.constants import ERROR_STACK +from ddtrace.constants import ERROR_TYPE +from ddtrace.context import Context as DatadogContext +from ddtrace.internal.compat import NumericType +from ddtrace.span import Span as DatadogSpan + +from .span_context import SpanContext +from .tags import Tags + + +if TYPE_CHECKING: + from .tracer import Tracer + + +_TagNameType = Union[Text, bytes] + + +class Span(OpenTracingSpan): + """Datadog implementation of :class:`opentracing.Span`""" + + def __init__(self, tracer, context, operation_name): + # type: (Tracer, Optional[SpanContext], str) -> None + if context is not None: + context = SpanContext(ddcontext=context._dd_context, baggage=context.baggage) + else: + context = SpanContext() + + super(Span, self).__init__(tracer, context) + + self.finished = False + self._lock = threading.Lock() + # use a datadog span + self._dd_span = DatadogSpan(tracer._dd_tracer, operation_name, context=context._dd_context) + + def finish(self, finish_time=None): + # type: (Optional[float]) -> None + """Finish the span. + + This calls finish on the ddspan. + + :param finish_time: specify a custom finish time with a unix timestamp + per time.time() + :type timestamp: float + """ + if self.finished: + return + + # finish the datadog span + self._dd_span.finish(finish_time) + self.finished = True + + def set_baggage_item(self, key, value): + # type: (str, Any) -> Span + """Sets a baggage item in the span context of this span. + + Baggage is used to propagate state between spans. + + :param key: baggage item key + :type key: str + + :param value: baggage item value + :type value: a type that can be compat.stringify()'d + + :rtype: Span + :return: itself for chaining calls + """ + new_ctx = self.context.with_baggage_item(key, value) + with self._lock: + self._context = new_ctx + return self + + def get_baggage_item(self, key): + # type: (str) -> Optional[str] + """Gets a baggage item from the span context of this span. + + :param key: baggage item key + :type key: str + + :rtype: str + :return: the baggage value for the given key or ``None``. + """ + return self.context.get_baggage_item(key) + + def set_operation_name(self, operation_name): + # type: (str) -> None + """Set the operation name.""" + self._dd_span.name = operation_name + + def log_kv(self, key_values, timestamp=None): + # type: (Dict[_TagNameType, Any], Optional[float]) -> Span + """Add a log record to this span. + + Passes on relevant opentracing key values onto the datadog span. + + :param key_values: a dict of string keys and values of any type + :type key_values: dict + + :param timestamp: a unix timestamp per time.time() + :type timestamp: float + + :return: the span itself, for call chaining + :rtype: Span + """ + + # match opentracing defined keys to datadog functionality + # opentracing/specification/blob/1be630515dafd4d2a468d083300900f89f28e24d/semantic_conventions.md#log-fields-table + for key, val in key_values.items(): + if key == "event" and val == "error": + # TODO: not sure if it's actually necessary to set the error manually + self._dd_span.error = 1 + self.set_tag("error", 1) + elif key == "error" or key == "error.object": + self.set_tag(ERROR_TYPE, val) + elif key == "message": + self.set_tag(ERROR_MSG, val) + elif key == "stack": + self.set_tag(ERROR_STACK, val) + else: + pass + + return self + + def set_tag(self, key, value): + # type: (_TagNameType, Any) -> None + """Set a tag on the span. + + This sets the tag on the underlying datadog span. + """ + if key == Tags.SPAN_TYPE: + self._dd_span.span_type = value + elif key == Tags.SERVICE_NAME: + self._dd_span.service = value + elif key == Tags.RESOURCE_NAME or key == OTTags.DATABASE_STATEMENT: + self._dd_span.resource = value + elif key == OTTags.PEER_HOSTNAME: + self._dd_span.set_tag(Tags.TARGET_HOST, value) + elif key == OTTags.PEER_PORT: + self._dd_span.set_tag(Tags.TARGET_PORT, value) + elif key == Tags.SAMPLING_PRIORITY: + self._dd_span.context.sampling_priority = value + else: + self._dd_span.set_tag(key, value) + + def _get_tag(self, key): + # type: (_TagNameType) -> Optional[Text] + """Gets a tag from the span. + + This method retrieves the tag from the underlying datadog span. + """ + return self._dd_span.get_tag(key) + + def _get_metric(self, key): + # type: (_TagNameType) -> Optional[NumericType] + """Gets a metric from the span. + + This method retrieves the metric from the underlying datadog span. + """ + return self._dd_span.get_metric(key) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type: + self._dd_span.set_exc_info(exc_type, exc_val, exc_tb) + + # note: self.finish() AND _dd_span.__exit__ will call _span.finish() but + # it is idempotent + self._dd_span.__exit__(exc_type, exc_val, exc_tb) + self.finish() + + def _associate_dd_span(self, ddspan): + # type: (DatadogSpan) -> None + """Associates a DD span with this span.""" + # get the datadog span context + self._dd_span = ddspan + self.context._dd_context = ddspan.context + + @property + def _dd_context(self): + # type: () -> DatadogContext + return self._dd_span.context diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/span_context.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/span_context.py new file mode 100644 index 000000000..164dbdb0e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/span_context.py @@ -0,0 +1,66 @@ +from typing import Any +from typing import Dict +from typing import Optional + +from opentracing import SpanContext as OpenTracingSpanContext + +from ddtrace.context import Context as DatadogContext +from ddtrace.internal.compat import NumericType + + +class SpanContext(OpenTracingSpanContext): + """Implementation of the OpenTracing span context.""" + + def __init__( + self, + trace_id=None, # type: Optional[int] + span_id=None, # type: Optional[int] + sampling_priority=None, # type: Optional[NumericType] + baggage=None, # type: Optional[Dict[str, Any]] + ddcontext=None, # type: Optional[DatadogContext] + ): + # type: (...) -> None + # create a new dict for the baggage if it is not provided + # NOTE: it would be preferable to use opentracing.SpanContext.EMPTY_BAGGAGE + # but it is mutable. + # see: opentracing-python/blob/8775c7bfc57fd66e1c8bcf9a54d3e434d37544f9/opentracing/span.py#L30 + baggage = baggage or {} + + if ddcontext is not None: + self._dd_context = ddcontext + else: + self._dd_context = DatadogContext( + trace_id=trace_id, + span_id=span_id, + sampling_priority=sampling_priority, + ) + + self._baggage = dict(baggage) + + @property + def baggage(self): + # type: () -> Dict[str, Any] + return self._baggage + + def set_baggage_item(self, key, value): + # type: (str, Any) -> None + """Sets a baggage item in this span context. + + Note that this operation mutates the baggage of this span context + """ + self.baggage[key] = value + + def with_baggage_item(self, key, value): + # type: (str, Any) -> SpanContext + """Returns a copy of this span with a new baggage item. + + Useful for instantiating new child span contexts. + """ + baggage = dict(self._baggage) + baggage[key] = value + return SpanContext(ddcontext=self._dd_context, baggage=baggage) + + def get_baggage_item(self, key): + # type: (str) -> Optional[Any] + """Gets a baggage item in this span context.""" + return self.baggage.get(key, None) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/tags.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/tags.py new file mode 100644 index 000000000..327fef1f6 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/tags.py @@ -0,0 +1,23 @@ +from collections import namedtuple + + +TagNames = namedtuple( + "TagNames", + [ + "RESOURCE_NAME", + "SAMPLING_PRIORITY", + "SERVICE_NAME", + "SPAN_TYPE", + "TARGET_HOST", + "TARGET_PORT", + ], +) + +Tags = TagNames( + RESOURCE_NAME="resource.name", + SAMPLING_PRIORITY="sampling.priority", + SERVICE_NAME="service.name", + TARGET_HOST="out.host", + TARGET_PORT="out.port", + SPAN_TYPE="span.type", +) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/tracer.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/tracer.py new file mode 100644 index 000000000..50d1069ec --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/tracer.py @@ -0,0 +1,375 @@ +from typing import Any +from typing import Dict +from typing import List +from typing import Optional +from typing import Union + +import opentracing +from opentracing import Format +from opentracing import Scope +from opentracing import ScopeManager +from opentracing.scope_managers import ThreadLocalScopeManager + +import ddtrace +from ddtrace import Span as DatadogSpan +from ddtrace import Tracer as DatadogTracer +from ddtrace.constants import FILTERS_KEY +from ddtrace.context import Context as DatadogContext +from ddtrace.settings import ConfigException +from ddtrace.utils.config import get_application_name + +from ..internal.logger import get_logger +from .propagation import HTTPPropagator +from .settings import ConfigKeys as keys +from .settings import config_invalid_keys +from .span import Span +from .span_context import SpanContext +from .utils import get_context_provider_for_scope_manager + + +log = get_logger(__name__) + +DEFAULT_CONFIG = { + keys.AGENT_HOSTNAME: None, + keys.AGENT_HTTPS: None, + keys.AGENT_PORT: None, + keys.DEBUG: False, + keys.ENABLED: None, + keys.GLOBAL_TAGS: {}, + keys.SAMPLER: None, + keys.PRIORITY_SAMPLING: None, + keys.UDS_PATH: None, + keys.SETTINGS: { + FILTERS_KEY: [], + }, +} # type: Dict[str, Any] + + +class Tracer(opentracing.Tracer): + """A wrapper providing an OpenTracing API for the Datadog tracer.""" + + def __init__( + self, + service_name=None, # type: Optional[str] + config=None, # type: Optional[Dict[str, Any]] + scope_manager=None, # type: Optional[ScopeManager] + dd_tracer=None, # type: Optional[DatadogTracer] + ): + # type: (...) -> None + """Initialize a new Datadog opentracer. + + :param service_name: (optional) the name of the service that this + tracer will be used with. Note if not provided, a service name will + try to be determined based off of ``sys.argv``. If this fails a + :class:`ddtrace.settings.ConfigException` will be raised. + :param config: (optional) a configuration object to specify additional + options. See the documentation for further information. + :param scope_manager: (optional) the scope manager for this tracer to + use. The available managers are listed in the Python OpenTracing repo + here: https://github.com/opentracing/opentracing-python#scope-managers. + If ``None`` is provided, defaults to + :class:`opentracing.scope_managers.ThreadLocalScopeManager`. + :param dd_tracer: (optional) the Datadog tracer for this tracer to use. This + should only be passed if a custom Datadog tracer is being used. Defaults + to the global ``ddtrace.tracer`` tracer. + """ + # Merge the given config with the default into a new dict + self._config = DEFAULT_CONFIG.copy() + if config is not None: + self._config.update(config) + # Pull out commonly used properties for performance + self._service_name = service_name or get_application_name() + self._debug = self._config.get(keys.DEBUG) + + if self._debug: + # Ensure there are no typos in any of the keys + invalid_keys = config_invalid_keys(self._config) + if invalid_keys: + str_invalid_keys = ",".join(invalid_keys) + raise ConfigException("invalid key(s) given ({})".format(str_invalid_keys)) + + if not self._service_name: + raise ConfigException( + """ Cannot detect the \'service_name\'. + Please set the \'service_name=\' + keyword argument. + """ + ) + + self._scope_manager = scope_manager or ThreadLocalScopeManager() + dd_context_provider = get_context_provider_for_scope_manager(self._scope_manager) + + self._dd_tracer = dd_tracer or ddtrace.tracer or DatadogTracer() + self._dd_tracer.set_tags(self._config.get(keys.GLOBAL_TAGS)) # type: ignore[arg-type] + self._dd_tracer.configure( + enabled=self._config.get(keys.ENABLED), + hostname=self._config.get(keys.AGENT_HOSTNAME), + https=self._config.get(keys.AGENT_HTTPS), + port=self._config.get(keys.AGENT_PORT), + sampler=self._config.get(keys.SAMPLER), + settings=self._config.get(keys.SETTINGS), + priority_sampling=self._config.get(keys.PRIORITY_SAMPLING), + uds_path=self._config.get(keys.UDS_PATH), + context_provider=dd_context_provider, # type: ignore[arg-type] + ) + self._propagators = { + Format.HTTP_HEADERS: HTTPPropagator, + Format.TEXT_MAP: HTTPPropagator, + } + + @property + def scope_manager(self): + # type: () -> ScopeManager + """Returns the scope manager being used by this tracer.""" + return self._scope_manager + + def start_active_span( + self, + operation_name, # type: str + child_of=None, # type: Optional[Union[Span, SpanContext]] + references=None, # type: Optional[List[Any]] + tags=None, # type: Optional[Dict[str, str]] + start_time=None, # type: Optional[int] + ignore_active_span=False, # type: bool + finish_on_close=True, # type: bool + ): + # type: (...) -> Scope + """Returns a newly started and activated `Scope`. + The returned `Scope` supports with-statement contexts. For example:: + + with tracer.start_active_span('...') as scope: + scope.span.set_tag('http.method', 'GET') + do_some_work() + # Span.finish() is called as part of Scope deactivation through + # the with statement. + + It's also possible to not finish the `Span` when the `Scope` context + expires:: + + with tracer.start_active_span('...', + finish_on_close=False) as scope: + scope.span.set_tag('http.method', 'GET') + do_some_work() + # Span.finish() is not called as part of Scope deactivation as + # `finish_on_close` is `False`. + + :param operation_name: name of the operation represented by the new + span from the perspective of the current service. + :param child_of: (optional) a Span or SpanContext instance representing + the parent in a REFERENCE_CHILD_OF Reference. If specified, the + `references` parameter must be omitted. + :param references: (optional) a list of Reference objects that identify + one or more parent SpanContexts. (See the Reference documentation + for detail). + :param tags: an optional dictionary of Span Tags. The caller gives up + ownership of that dictionary, because the Tracer may use it as-is + to avoid extra data copying. + :param start_time: an explicit Span start time as a unix timestamp per + time.time(). + :param ignore_active_span: (optional) an explicit flag that ignores + the current active `Scope` and creates a root `Span`. + :param finish_on_close: whether span should automatically be finished + when `Scope.close()` is called. + :return: a `Scope`, already registered via the `ScopeManager`. + """ + otspan = self.start_span( + operation_name=operation_name, + child_of=child_of, + references=references, + tags=tags, + start_time=start_time, + ignore_active_span=ignore_active_span, + ) + + # activate this new span + scope = self._scope_manager.activate(otspan, finish_on_close) + self._dd_tracer.context_provider.activate(otspan._dd_span) + return scope + + def start_span( + self, + operation_name=None, # type: Optional[str] + child_of=None, # type: Optional[Union[Span, SpanContext]] + references=None, # type: Optional[List[Any]] + tags=None, # type: Optional[Dict[str, str]] + start_time=None, # type: Optional[int] + ignore_active_span=False, # type: bool + ): + # type: (...) -> Span + """Starts and returns a new Span representing a unit of work. + + Starting a root Span (a Span with no causal references):: + + tracer.start_span('...') + + Starting a child Span (see also start_child_span()):: + + tracer.start_span( + '...', + child_of=parent_span) + + Starting a child Span in a more verbose way:: + + tracer.start_span( + '...', + references=[opentracing.child_of(parent_span)]) + + Note: the precedence when defining a relationship is the following, from highest to lowest: + 1. *child_of* + 2. *references* + 3. `scope_manager.active` (unless *ignore_active_span* is True) + 4. None + + Currently Datadog only supports `child_of` references. + + :param operation_name: name of the operation represented by the new + span from the perspective of the current service. + :param child_of: (optional) a Span or SpanContext instance representing + the parent in a REFERENCE_CHILD_OF Reference. If specified, the + `references` parameter must be omitted. + :param references: (optional) a list of Reference objects that identify + one or more parent SpanContexts. (See the Reference documentation + for detail) + :param tags: an optional dictionary of Span Tags. The caller gives up + ownership of that dictionary, because the Tracer may use it as-is + to avoid extra data copying. + :param start_time: an explicit Span start time as a unix timestamp per + time.time() + :param ignore_active_span: an explicit flag that ignores the current + active `Scope` and creates a root `Span`. + :return: an already-started Span instance. + """ + ot_parent = None # 'ot_parent' is more readable than 'child_of' + ot_parent_context = None # the parent span's context + # dd_parent: the child_of to pass to the ddtracer + dd_parent = None # type: Optional[Union[DatadogSpan, DatadogContext]] + + if child_of is not None: + ot_parent = child_of # 'ot_parent' is more readable than 'child_of' + elif references and isinstance(references, list): + # we currently only support child_of relations to one span + ot_parent = references[0].referenced_context + + # - whenever child_of is not None ddspans with parent-child + # relationships will share a ddcontext which maintains a hierarchy of + # ddspans for the execution flow + # - when child_of is a ddspan then the ddtracer uses this ddspan to + # create the child ddspan + # - when child_of is a ddcontext then the ddtracer uses the ddcontext to + # get_current_span() for the parent + if ot_parent is None and not ignore_active_span: + # attempt to get the parent span from the scope manager + scope = self._scope_manager.active + parent_span = getattr(scope, "span", None) + ot_parent_context = getattr(parent_span, "context", None) + + # Compare the active ot and dd spans. Using the one which + # was created later as the parent. + active_dd_parent = self._dd_tracer.context_provider.active() + if parent_span and isinstance(active_dd_parent, DatadogSpan): + dd_parent_span = parent_span._dd_span + if active_dd_parent.start_ns >= dd_parent_span.start_ns: + dd_parent = active_dd_parent + else: + dd_parent = dd_parent_span + else: + dd_parent = active_dd_parent + elif ot_parent is not None and isinstance(ot_parent, Span): + # a span is given to use as a parent + ot_parent_context = ot_parent.context + dd_parent = ot_parent._dd_span + elif ot_parent is not None and isinstance(ot_parent, SpanContext): + # a span context is given to use to find the parent ddspan + dd_parent = ot_parent._dd_context + elif ot_parent is None: + # user wants to create a new parent span we don't have to do + # anything + pass + else: + raise TypeError("invalid span configuration given") + + # create a new otspan and ddspan using the ddtracer and associate it + # with the new otspan + ddspan = self._dd_tracer.start_span( + name=operation_name, # type: ignore[arg-type] + child_of=dd_parent, + service=self._service_name, + activate=False, + ) + + # set the start time if one is specified + ddspan.start = start_time or ddspan.start + + otspan = Span(self, ot_parent_context, operation_name) # type: ignore[arg-type] + # sync up the OT span with the DD span + otspan._associate_dd_span(ddspan) + + if tags is not None: + for k in tags: + # Make sure we set the tags on the otspan to ensure that the special compatibility tags + # are handled correctly (resource name, span type, sampling priority, etc). + otspan.set_tag(k, tags[k]) + + return otspan + + @property + def active_span(self): + # type: () -> Optional[Span] + """Retrieves the active span from the opentracing scope manager + + Falls back to using the datadog active span if one is not found. This + allows opentracing users to use datadog instrumentation. + """ + scope = self._scope_manager.active + if scope: + return scope.span + else: + dd_span = self._dd_tracer.current_span() + ot_span = None # type: Optional[Span] + if dd_span: + ot_span = Span(self, None, dd_span.name) + ot_span._associate_dd_span(dd_span) + return ot_span + + def inject(self, span_context, format, carrier): # noqa: A002 + # type: (SpanContext, str, Dict[str, str]) -> None + """Injects a span context into a carrier. + + :param span_context: span context to inject. + :param format: format to encode the span context with. + :param carrier: the carrier of the encoded span context. + """ + propagator = self._propagators.get(format, None) + + if propagator is None: + raise opentracing.UnsupportedFormatException + + propagator.inject(span_context, carrier) + + def extract(self, format, carrier): # noqa: A002 + # type: (str, Dict[str, str]) -> SpanContext + """Extracts a span context from a carrier. + + :param format: format that the carrier is encoded with. + :param carrier: the carrier to extract from. + """ + propagator = self._propagators.get(format, None) + + if propagator is None: + raise opentracing.UnsupportedFormatException + + # we have to manually activate the returned context from a distributed + # trace + ot_span_ctx = propagator.extract(carrier) + dd_span_ctx = ot_span_ctx._dd_context + self._dd_tracer.context_provider.activate(dd_span_ctx) + return ot_span_ctx + + def get_log_correlation_context(self): + # type: () -> Dict[str, str] + """Retrieves the data used to correlate a log with the current active trace. + Generates a dictionary for custom logging instrumentation including the trace id and + span id of the current active span, as well as the configured service, version, and environment names. + If there is no active span, a dictionary with an empty string for each value will be returned. + """ + return self._dd_tracer.get_log_correlation_context() diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/utils.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/utils.py new file mode 100644 index 000000000..bca1b8718 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/opentracer/utils.py @@ -0,0 +1,60 @@ +from opentracing import ScopeManager + +from ddtrace.provider import BaseContextProvider + + +# DEV: If `asyncio` or `gevent` are unavailable we do not throw an error, +# `context_provider` will just not be set and we'll get an `AttributeError` instead + + +def get_context_provider_for_scope_manager(scope_manager): + # type: (ScopeManager) -> BaseContextProvider + """Returns the context_provider to use with a given scope_manager.""" + + scope_manager_type = type(scope_manager).__name__ + + # avoid having to import scope managers which may not be compatible + # with the version of python being used + if scope_manager_type == "AsyncioScopeManager": + import ddtrace.contrib.asyncio + + dd_context_provider = ddtrace.contrib.asyncio.context_provider # type: BaseContextProvider + elif scope_manager_type == "GeventScopeManager": + import ddtrace.contrib.gevent + + dd_context_provider = ddtrace.contrib.gevent.context_provider + else: + from ddtrace.provider import DefaultContextProvider + + dd_context_provider = DefaultContextProvider() + + _patch_scope_manager(scope_manager, dd_context_provider) + + return dd_context_provider + + +def _patch_scope_manager(scope_manager, context_provider): + # type: (ScopeManager, BaseContextProvider) -> None + """ + Patches a scope manager so that any time a span is activated + it'll also activate the underlying ddcontext with the underlying + datadog context provider. + + This allows opentracing users to rely on ddtrace.contrib patches and + have them parent correctly. + + :param scope_manager: Something that implements `opentracing.ScopeManager` + :param context_provider: Something that implements `datadog.provider.BaseContextProvider` + """ + if getattr(scope_manager, "_datadog_patch", False): + return + setattr(scope_manager, "_datadog_patch", True) + + old_method = scope_manager.activate + + def _patched_activate(*args, **kwargs): + otspan = kwargs.get("span", args[0]) + context_provider.activate(otspan._dd_context) + return old_method(*args, **kwargs) + + scope_manager.activate = _patched_activate diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/pin.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/pin.py new file mode 100644 index 000000000..f16d170cd --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/pin.py @@ -0,0 +1,219 @@ +from typing import Any +from typing import Dict +from typing import Optional +from typing import TYPE_CHECKING + +import ddtrace +from ddtrace.vendor import debtcollector + +from .internal.logger import get_logger +from .vendor import wrapt + + +if TYPE_CHECKING: + from .tracer import Tracer + + +log = get_logger(__name__) + + +# To set attributes on wrapt proxy objects use this prefix: +# http://wrapt.readthedocs.io/en/latest/wrappers.html +_DD_PIN_NAME = "_datadog_pin" +_DD_PIN_PROXY_NAME = "_self_" + _DD_PIN_NAME + + +class Pin(object): + """Pin (a.k.a Patch INfo) is a small class which is used to + set tracing metadata on a particular traced connection. + This is useful if you wanted to, say, trace two different + database clusters. + + >>> conn = sqlite.connect('/tmp/user.db') + >>> # Override a pin for a specific connection + >>> pin = Pin.override(conn, service='user-db') + >>> conn = sqlite.connect('/tmp/image.db') + """ + + __slots__ = ["app", "tags", "tracer", "_target", "_config", "_initialized"] + + @debtcollector.removals.removed_kwarg("app_type") + def __init__( + self, + service=None, # type: Optional[str] + app=None, # type: Optional[str] + app_type=None, + tags=None, # type: Optional[Dict[str, str]] + tracer=None, # type: Optional[Tracer] + _config=None, # type: Optional[Dict[str, Any]] + ): + # type: (...) -> None + tracer = tracer or ddtrace.tracer + self.app = app + self.tags = tags + self.tracer = tracer + self._target = None # type: Optional[int] + # keep the configuration attribute internal because the + # public API to access it is not the Pin class + self._config = _config or {} # type: Dict[str, Any] + # [Backward compatibility]: service argument updates the `Pin` config + self._config["service_name"] = service + self._initialized = True + + @property + def service(self): + # type: () -> str + """Backward compatibility: accessing to `pin.service` returns the underlying + configuration value. + """ + return self._config["service_name"] + + def __setattr__(self, name, value): + if getattr(self, "_initialized", False) and name != "_target": + raise AttributeError("can't mutate a pin, use override() or clone() instead") + super(Pin, self).__setattr__(name, value) + + def __repr__(self): + return "Pin(service=%s, app=%s, tags=%s, tracer=%s)" % (self.service, self.app, self.tags, self.tracer) + + @staticmethod + def _find(*objs): + # type: (Any) -> Optional[Pin] + """ + Return the first :class:`ddtrace.pin.Pin` found on any of the provided objects or `None` if none were found + + + >>> pin = Pin._find(wrapper, instance, conn, app) + + :param objs: The objects to search for a :class:`ddtrace.pin.Pin` on + :type objs: List of objects + :rtype: :class:`ddtrace.pin.Pin`, None + :returns: The first found :class:`ddtrace.pin.Pin` or `None` is none was found + """ + for obj in objs: + pin = Pin.get_from(obj) + if pin: + return pin + return None + + @staticmethod + def get_from(obj): + # type: (Any) -> Optional[Pin] + """Return the pin associated with the given object. If a pin is attached to + `obj` but the instance is not the owner of the pin, a new pin is cloned and + attached. This ensures that a pin inherited from a class is a copy for the new + instance, avoiding that a specific instance overrides other pins values. + + >>> pin = Pin.get_from(conn) + + :param obj: The object to look for a :class:`ddtrace.pin.Pin` on + :type obj: object + :rtype: :class:`ddtrace.pin.Pin`, None + :returns: :class:`ddtrace.pin.Pin` associated with the object, or None if none was found + """ + if hasattr(obj, "__getddpin__"): + return obj.__getddpin__() + + pin_name = _DD_PIN_PROXY_NAME if isinstance(obj, wrapt.ObjectProxy) else _DD_PIN_NAME + pin = getattr(obj, pin_name, None) + # detect if the PIN has been inherited from a class + if pin is not None and pin._target != id(obj): + pin = pin.clone() + pin.onto(obj) + return pin + + @classmethod + @debtcollector.removals.removed_kwarg("app_type") + def override( + cls, + obj, # type: Any + service=None, # type: Optional[str] + app=None, # type: Optional[str] + app_type=None, + tags=None, # type: Optional[Dict[str, str]] + tracer=None, # type: Optional[Tracer] + ): + # type: (...) -> None + """Override an object with the given attributes. + + That's the recommended way to customize an already instrumented client, without + losing existing attributes. + + >>> conn = sqlite.connect('/tmp/user.db') + >>> # Override a pin for a specific connection + >>> Pin.override(conn, service='user-db') + """ + if not obj: + return + + pin = cls.get_from(obj) + if pin is None: + Pin(service=service, app=app, tags=tags, tracer=tracer).onto(obj) + else: + pin.clone(service=service, app=app, tags=tags, tracer=tracer).onto(obj) + + def enabled(self): + # type: () -> bool + """Return true if this pin's tracer is enabled.""" + return bool(self.tracer) and self.tracer.enabled + + def onto(self, obj, send=True): + # type: (Any, bool) -> None + """Patch this pin onto the given object. If send is true, it will also + queue the metadata to be sent to the server. + """ + # Actually patch it on the object. + try: + if hasattr(obj, "__setddpin__"): + return obj.__setddpin__(self) + + pin_name = _DD_PIN_PROXY_NAME if isinstance(obj, wrapt.ObjectProxy) else _DD_PIN_NAME + + # set the target reference; any get_from, clones and retarget the new PIN + self._target = id(obj) + return setattr(obj, pin_name, self) + except AttributeError: + log.debug("can't pin onto object. skipping", exc_info=True) + + def remove_from(self, obj): + # type: (Any) -> None + # Remove pin from the object. + try: + pin_name = _DD_PIN_PROXY_NAME if isinstance(obj, wrapt.ObjectProxy) else _DD_PIN_NAME + + pin = Pin.get_from(obj) + if pin is not None: + delattr(obj, pin_name) + except AttributeError: + log.debug("can't remove pin from object. skipping", exc_info=True) + + @debtcollector.removals.removed_kwarg("app_type") + def clone( + self, + service=None, # type: Optional[str] + app=None, # type: Optional[str] + app_type=None, + tags=None, # type: Optional[Dict[str, str]] + tracer=None, # type: Optional[Tracer] + ): + # type: (...) -> Pin + """Return a clone of the pin with the given attributes replaced.""" + # do a shallow copy of Pin dicts + if not tags and self.tags: + tags = self.tags.copy() + + # we use a copy instead of a deepcopy because we expect configurations + # to have only a root level dictionary without nested objects. Using + # deepcopy introduces a big overhead: + # + # copy: 0.00654911994934082 + # deepcopy: 0.2787208557128906 + config = self._config.copy() + + return Pin( + service=service or self.service, + app=app or self.app, + tags=tags, + tracer=tracer or self.tracer, # do not clone the Tracer + _config=config, + ) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__init__.py new file mode 100644 index 000000000..3361b574d --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__init__.py @@ -0,0 +1,21 @@ +import sys + +from ddtrace.profiling import _build + +from .profiler import Profiler # noqa:F401 + + +def _not_compatible_abi(): + raise ImportError( + "Python ABI is not compatible, you need to recompile this module.\n" + "Reinstall it with the following command:\n" + " pip install --no-binary ddtrace ddtrace" + ) + + +if (3, 7) < _build.compiled_with <= (3, 7, 3): + if sys.version_info[:3] > (3, 7, 3): + _not_compatible_abi() +elif (3, 7, 3) < _build.compiled_with < (3, 8): + if (3, 7) < sys.version_info[:3] <= (3, 7, 3): + _not_compatible_abi() diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..af8c67288 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/_traceback.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/_traceback.cpython-38.pyc new file mode 100644 index 000000000..b0e51533b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/_traceback.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/auto.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/auto.cpython-38.pyc new file mode 100644 index 000000000..9b8a53b8c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/auto.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/event.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/event.cpython-38.pyc new file mode 100644 index 000000000..d3c857a3c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/event.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/profiler.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/profiler.cpython-38.pyc new file mode 100644 index 000000000..6b1590f86 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/profiler.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/recorder.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/recorder.cpython-38.pyc new file mode 100644 index 000000000..9c83698ce Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/recorder.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/scheduler.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/scheduler.cpython-38.pyc new file mode 100644 index 000000000..d48426d3e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/__pycache__/scheduler.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/_build.cpython-38-darwin.so b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/_build.cpython-38-darwin.so new file mode 100755 index 000000000..6e34ed1c8 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/_build.cpython-38-darwin.so differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/_traceback.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/_traceback.py new file mode 100644 index 000000000..130df7cca --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/_traceback.py @@ -0,0 +1,5 @@ +import traceback + + +def format_exception(e): + return traceback.format_exception_only(type(e), e)[0].rstrip() diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/auto.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/auto.py new file mode 100644 index 000000000..59d666fb5 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/auto.py @@ -0,0 +1,5 @@ +"""Automatically starts a collector when imported.""" +from ddtrace.profiling.bootstrap import sitecustomize # noqa + + +start_profiler = sitecustomize.start_profiler diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/bootstrap/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/bootstrap/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/bootstrap/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/bootstrap/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..23ce48dec Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/bootstrap/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/bootstrap/__pycache__/sitecustomize.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/bootstrap/__pycache__/sitecustomize.cpython-38.pyc new file mode 100644 index 000000000..f0937ff2b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/bootstrap/__pycache__/sitecustomize.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/bootstrap/sitecustomize.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/bootstrap/sitecustomize.py new file mode 100644 index 000000000..c186df036 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/bootstrap/sitecustomize.py @@ -0,0 +1,15 @@ +# -*- encoding: utf-8 -*- +"""Bootstrapping code that is run when using `ddtrace.profiling.auto`.""" +from ddtrace.profiling import bootstrap +from ddtrace.profiling import profiler + + +def start_profiler(): + if hasattr(bootstrap, "profiler"): + bootstrap.profiler.stop() + # Export the profiler so we can introspect it if needed + bootstrap.profiler = profiler.Profiler() + bootstrap.profiler.start() + + +start_profiler() diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/__init__.py new file mode 100644 index 000000000..77e686f80 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/__init__.py @@ -0,0 +1,81 @@ +# -*- encoding: utf-8 -*- +import typing + +import attr + +from ddtrace.internal import periodic +from ddtrace.internal import service +from ddtrace.utils import attr as attr_utils + +from .. import event + + +class CollectorError(Exception): + pass + + +class CollectorUnavailable(CollectorError): + pass + + +@attr.s +class Collector(service.Service): + """A profile collector.""" + + recorder = attr.ib() + + @staticmethod + def snapshot(): + """Take a snapshot of collected data. + + :return: A list of sample list to push in the recorder. + """ + + +@attr.s(slots=True) +class PeriodicCollector(Collector, periodic.PeriodicService): + """A collector that needs to run periodically.""" + + def periodic(self): + # type: (...) -> None + """Collect events and push them into the recorder.""" + for events in self.collect(): + self.recorder.push_events(events) + + def collect(self): + # type: (...) -> typing.Iterable[typing.Iterable[event.Event]] + """Collect the actual data. + + :return: A list of event list to push in the recorder. + """ + raise NotImplementedError + + +@attr.s +class CaptureSampler(object): + """Determine the events that should be captured based on a sampling percentage.""" + + capture_pct = attr.ib(default=100) + _counter = attr.ib(default=0, init=False) + + @capture_pct.validator + def capture_pct_validator(self, attribute, value): + if value < 0 or value > 100: + raise ValueError("Capture percentage should be between 0 and 100 included") + + def capture(self): + self._counter += self.capture_pct + if self._counter >= 100: + self._counter -= 100 + return True + return False + + +def _create_capture_sampler(collector): + return CaptureSampler(collector.capture_pct) + + +@attr.s +class CaptureSamplerCollector(Collector): + capture_pct = attr.ib(factory=attr_utils.from_env("DD_PROFILING_CAPTURE_PCT", 2.0, float)) + _capture_sampler = attr.ib(default=attr.Factory(_create_capture_sampler, takes_self=True), init=False, repr=False) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..9f5cb2282 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/__pycache__/memalloc.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/__pycache__/memalloc.cpython-38.pyc new file mode 100644 index 000000000..784247a25 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/__pycache__/memalloc.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/__pycache__/threading.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/__pycache__/threading.cpython-38.pyc new file mode 100644 index 000000000..f76088d3e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/__pycache__/threading.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/_memalloc.cpython-38-darwin.so b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/_memalloc.cpython-38-darwin.so new file mode 100755 index 000000000..16051157b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/_memalloc.cpython-38-darwin.so differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/_task.cpython-38-darwin.so b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/_task.cpython-38-darwin.so new file mode 100755 index 000000000..2e5e7e15a Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/_task.cpython-38-darwin.so differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/_threading.cpython-38-darwin.so b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/_threading.cpython-38-darwin.so new file mode 100755 index 000000000..565d8d562 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/_threading.cpython-38-darwin.so differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/_traceback.cpython-38-darwin.so b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/_traceback.cpython-38-darwin.so new file mode 100755 index 000000000..795231672 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/_traceback.cpython-38-darwin.so differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/memalloc.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/memalloc.py new file mode 100644 index 000000000..ecdb52eec --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/memalloc.py @@ -0,0 +1,166 @@ +# -*- encoding: utf-8 -*- +import logging +import math +import os +import threading +import typing + +import attr + + +try: + from ddtrace.profiling.collector import _memalloc +except ImportError: + _memalloc = None # type: ignore[assignment] + +from ddtrace.profiling import collector +from ddtrace.profiling import event +from ddtrace.profiling.collector import _threading +from ddtrace.utils import attr as attr_utils +from ddtrace.utils import formats + + +LOG = logging.getLogger(__name__) + + +@event.event_class +class MemoryAllocSampleEvent(event.StackBasedEvent): + """A sample storing memory allocation tracked.""" + + size = attr.ib(default=None) + """Allocation size in bytes.""" + + capture_pct = attr.ib(default=None) + """The capture percentage.""" + + nevents = attr.ib(default=None) + """The total number of allocation events sampled.""" + + +@event.event_class +class MemoryHeapSampleEvent(event.StackBasedEvent): + """A sample storing memory allocation tracked.""" + + size = attr.ib(default=None) + """Allocation size in bytes.""" + + sample_size = attr.ib(default=None) + """The sampling size.""" + + +def _get_default_heap_sample_size( + default_heap_sample_size=512 * 1024, # type: int +): + # type: (...) -> int + heap_sample_size = os.environ.get("DD_PROFILING_HEAP_SAMPLE_SIZE") + if heap_sample_size is not None: + return int(heap_sample_size) + + if not formats.asbool(os.environ.get("DD_PROFILING_HEAP_ENABLED", "0")): + return 0 + + try: + from ddtrace.vendor import psutil + + total_mem = psutil.swap_memory().total + psutil.virtual_memory().total + except Exception: + LOG.warning( + "Unable to get total memory available, using default value of %d KB", + default_heap_sample_size / 1024, + exc_info=True, + ) + return default_heap_sample_size + + # This is TRACEBACK_ARRAY_MAX_COUNT + max_samples = 2 ** 16 + + return max(math.ceil(total_mem / max_samples), default_heap_sample_size) + + +@attr.s +class MemoryCollector(collector.PeriodicCollector): + """Memory allocation collector.""" + + _DEFAULT_MAX_EVENTS = 32 + _DEFAULT_INTERVAL = 0.5 + + # Arbitrary interval to empty the _memalloc event buffer + _interval = attr.ib(default=_DEFAULT_INTERVAL, repr=False) + + # TODO make this dynamic based on the 1. interval and 2. the max number of events allowed in the Recorder + _max_events = attr.ib(factory=attr_utils.from_env("_DD_PROFILING_MEMORY_EVENTS_BUFFER", _DEFAULT_MAX_EVENTS, int)) + max_nframe = attr.ib(factory=attr_utils.from_env("DD_PROFILING_MAX_FRAMES", 64, int)) + heap_sample_size = attr.ib(type=int, factory=_get_default_heap_sample_size) + ignore_profiler = attr.ib(factory=attr_utils.from_env("DD_PROFILING_IGNORE_PROFILER", False, formats.asbool)) + + def _start_service(self): # type: ignore[override] + # type: (...) -> None + """Start collecting memory profiles.""" + if _memalloc is None: + raise collector.CollectorUnavailable + + _memalloc.start(self.max_nframe, self._max_events, self.heap_sample_size) + + super(MemoryCollector, self)._start_service() + + def _stop_service(self): # type: ignore[override] + # type: (...) -> None + super(MemoryCollector, self)._stop_service() + + if _memalloc is not None: + try: + _memalloc.stop() + except RuntimeError: + pass + + def _get_thread_id_ignore_set(self): + # type: () -> typing.Set[int] + # This method is not perfect and prone to race condition in theory, but very little in practice. + # Anyhow it's not a big deal — it's a best effort feature. + return { + thread.ident + for thread in threading.enumerate() + if getattr(thread, "_ddtrace_profiling_ignore", False) and thread.ident is not None + } + + def snapshot(self): + thread_id_ignore_set = self._get_thread_id_ignore_set() + return ( + tuple( + MemoryHeapSampleEvent( + thread_id=thread_id, + thread_name=_threading.get_thread_name(thread_id), + thread_native_id=_threading.get_thread_native_id(thread_id), + frames=stack, + nframes=nframes, + size=size, + sample_size=self.heap_sample_size, + ) + for (stack, nframes, thread_id), size in _memalloc.heap() + if not self.ignore_profiler or thread_id not in thread_id_ignore_set + ), + ) + + def collect(self): + events, count, alloc_count = _memalloc.iter_events() + capture_pct = 100 * count / alloc_count + thread_id_ignore_set = self._get_thread_id_ignore_set() + # TODO: The event timestamp is slightly off since it's going to be the time we copy the data from the + # _memalloc buffer to our Recorder. This is fine for now, but we might want to store the nanoseconds + # timestamp in C and then return it via iter_events. + return ( + tuple( + MemoryAllocSampleEvent( + thread_id=thread_id, + thread_name=_threading.get_thread_name(thread_id), + thread_native_id=_threading.get_thread_native_id(thread_id), + frames=stack, + nframes=nframes, + size=size, + capture_pct=capture_pct, + nevents=alloc_count, + ) + for (stack, nframes, thread_id), size in events + if not self.ignore_profiler or thread_id not in thread_id_ignore_set + ), + ) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/stack.cpython-38-darwin.so b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/stack.cpython-38-darwin.so new file mode 100755 index 000000000..1e4c3a522 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/stack.cpython-38-darwin.so differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/threading.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/threading.py new file mode 100644 index 000000000..b5e0a7259 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/collector/threading.py @@ -0,0 +1,194 @@ +from __future__ import absolute_import + +import os.path +import sys +import threading +import typing + +import attr + +from ddtrace.internal import compat +from ddtrace.internal import nogevent +from ddtrace.profiling import collector +from ddtrace.profiling import event +from ddtrace.profiling.collector import _task +from ddtrace.profiling.collector import _threading +from ddtrace.profiling.collector import _traceback +from ddtrace.utils import attr as attr_utils +from ddtrace.utils import formats +from ddtrace.vendor import wrapt + + +@event.event_class +class LockEventBase(event.StackBasedEvent): + """Base Lock event.""" + + lock_name = attr.ib(default=None) + sampling_pct = attr.ib(default=None) + + +@event.event_class +class LockAcquireEvent(LockEventBase): + """A lock has been acquired.""" + + wait_time_ns = attr.ib(default=None) + + +@event.event_class +class LockReleaseEvent(LockEventBase): + """A lock has been released.""" + + locked_for_ns = attr.ib(default=None) + + +def _current_thread(): + # type: (...) -> typing.Tuple[int, str] + thread_id = nogevent.thread_get_ident() + return thread_id, _threading.get_thread_name(thread_id) + + +# We need to know if wrapt is compiled in C or not. If it's not using the C module, then the wrappers function will +# appear in the stack trace and we need to hide it. +if os.environ.get("WRAPT_DISABLE_EXTENSIONS"): + WRAPT_C_EXT = False +else: + try: + import ddtrace.vendor.wrapt._wrappers as _w # noqa: F401 + except ImportError: + WRAPT_C_EXT = False + else: + WRAPT_C_EXT = True + del _w + + +class _ProfiledLock(wrapt.ObjectProxy): + def __init__(self, wrapped, recorder, tracer, max_nframes, capture_sampler, endpoint_collection_enabled): + wrapt.ObjectProxy.__init__(self, wrapped) + self._self_recorder = recorder + self._self_tracer = tracer + self._self_max_nframes = max_nframes + self._self_capture_sampler = capture_sampler + self._self_endpoint_collection_enabled = endpoint_collection_enabled + frame = sys._getframe(2 if WRAPT_C_EXT else 3) + code = frame.f_code + self._self_name = "%s:%d" % (os.path.basename(code.co_filename), frame.f_lineno) + + def acquire(self, *args, **kwargs): + if not self._self_capture_sampler.capture(): + return self.__wrapped__.acquire(*args, **kwargs) + + start = compat.monotonic_ns() + try: + return self.__wrapped__.acquire(*args, **kwargs) + finally: + try: + end = self._self_acquired_at = compat.monotonic_ns() + thread_id, thread_name = _current_thread() + frames, nframes = _traceback.pyframe_to_frames(sys._getframe(1), self._self_max_nframes) + task_id, task_name = _task.get_task(thread_id) + event = LockAcquireEvent( + lock_name=self._self_name, + frames=frames, + nframes=nframes, + thread_id=thread_id, + thread_name=thread_name, + task_id=task_id, + task_name=task_name, + wait_time_ns=end - start, + sampling_pct=self._self_capture_sampler.capture_pct, + ) + + if self._self_tracer is not None: + event.set_trace_info(self._self_tracer.current_span(), self._self_endpoint_collection_enabled) + + self._self_recorder.push_event(event) + except Exception: + pass + + def release(self, *args, **kwargs): + try: + return self.__wrapped__.release(*args, **kwargs) + finally: + try: + if hasattr(self, "_self_acquired_at"): + try: + end = compat.monotonic_ns() + frames, nframes = _traceback.pyframe_to_frames(sys._getframe(1), self._self_max_nframes) + thread_id, thread_name = _current_thread() + task_id, task_name = _task.get_task(thread_id) + event = LockReleaseEvent( + lock_name=self._self_name, + frames=frames, + nframes=nframes, + thread_id=thread_id, + thread_name=thread_name, + task_id=task_id, + task_name=task_name, + locked_for_ns=end - self._self_acquired_at, + sampling_pct=self._self_capture_sampler.capture_pct, + ) + + if self._self_tracer is not None: + event.set_trace_info( + self._self_tracer.current_span(), self._self_endpoint_collection_enabled + ) + + self._self_recorder.push_event(event) + finally: + del self._self_acquired_at + except Exception: + pass + + acquire_lock = acquire + + +class FunctionWrapper(wrapt.FunctionWrapper): + # Override the __get__ method: whatever happens, _allocate_lock is always considered by Python like a "static" + # method, even when used as a class attribute. Python never tried to "bind" it to a method, because it sees it is a + # builtin function. Override default wrapt behavior here that tries to detect bound method. + def __get__(self, instance, owner=None): + return self + + +@attr.s +class LockCollector(collector.CaptureSamplerCollector): + """Record lock usage.""" + + nframes = attr.ib(factory=attr_utils.from_env("DD_PROFILING_MAX_FRAMES", 64, int)) + endpoint_collection_enabled = attr.ib( + factory=attr_utils.from_env("DD_PROFILING_ENDPOINT_COLLECTION_ENABLED", True, formats.asbool) + ) + + tracer = attr.ib(default=None) + + def _start_service(self): # type: ignore[override] + # type: (...) -> None + """Start collecting `threading.Lock` usage.""" + self.patch() + super(LockCollector, self)._start_service() + + def _stop_service(self): # type: ignore[override] + # type: (...) -> None + """Stop collecting `threading.Lock` usage.""" + super(LockCollector, self)._stop_service() + self.unpatch() + + def patch(self): + # type: (...) -> None + """Patch the threading module for tracking lock allocation.""" + # We only patch the lock from the `threading` module. + # Nobody should use locks from `_thread`; if they do so, then it's deliberate and we don't profile. + self.original = threading.Lock + + def _allocate_lock(wrapped, instance, args, kwargs): + lock = wrapped(*args, **kwargs) + return _ProfiledLock( + lock, self.recorder, self.tracer, self.nframes, self._capture_sampler, self.endpoint_collection_enabled + ) + + threading.Lock = FunctionWrapper(self.original, _allocate_lock) # type: ignore[misc] + + def unpatch(self): + # type: (...) -> None + """Unpatch the threading module for tracking lock allocation.""" + threading.Lock = self.original # type: ignore[misc] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/event.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/event.py new file mode 100644 index 000000000..c5ae2a0a2 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/event.py @@ -0,0 +1,64 @@ +import typing + +import attr + +from ddtrace import span as ddspan +from ddtrace.internal import compat + + +def event_class(klass): + return attr.s(slots=True)(klass) + + +@event_class +class Event(object): + """An event happening at a point in time.""" + + timestamp = attr.ib(factory=compat.time_ns) + + @property + def name(self): + """Name of the event.""" + return self.__class__.__name__ + + +@event_class +class TimedEvent(Event): + """An event that has a duration.""" + + duration = attr.ib(default=None) + + +@event_class +class SampleEvent(Event): + """An event representing a sample gathered from the system.""" + + sampling_period = attr.ib(default=None) + + +@event_class +class StackBasedEvent(SampleEvent): + thread_id = attr.ib(default=None) + thread_name = attr.ib(default=None) + thread_native_id = attr.ib(default=None) + task_id = attr.ib(default=None) + task_name = attr.ib(default=None) + frames = attr.ib(default=None) + nframes = attr.ib(default=None) + local_root_span_id = attr.ib(default=None, type=typing.Optional[int]) + span_id = attr.ib(default=None, type=typing.Optional[int]) + trace_type = attr.ib(default=None, type=typing.Optional[str]) + trace_resource_container = attr.ib(default=None) + + def set_trace_info( + self, + span, # type: typing.Optional[ddspan.Span] + endpoint_collection_enabled, # type: bool + ): + if span: + self.span_id = span.span_id + if span._local_root is not None: + self.local_root_span_id = span._local_root.span_id + self.trace_type = span._local_root._span_type + if endpoint_collection_enabled: + self.trace_resource_container = span._local_root._resource diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__init__.py new file mode 100644 index 000000000..c67af713f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__init__.py @@ -0,0 +1,30 @@ +import attr + + +class ExportError(Exception): + pass + + +@attr.s +class Exporter(object): + """Exporter base class.""" + + def export(self, events, start_time_ns, end_time_ns): + # type: (...) -> None + """Export events. + + :param events: List of events to export. + :param start_time_ns: The start time of recording. + :param end_time_ns: The end time of recording. + """ + raise NotImplementedError + + +@attr.s +class NullExporter(Exporter): + """Exporter that does nothing.""" + + def export(self, events, start_time_ns, end_time_ns): + # type: (...) -> None + """Discard events.""" + pass diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..50dc9c8cf Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__pycache__/file.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__pycache__/file.cpython-38.pyc new file mode 100644 index 000000000..21e9f1d93 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__pycache__/file.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__pycache__/http.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__pycache__/http.cpython-38.pyc new file mode 100644 index 000000000..a5ed51fe3 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__pycache__/http.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__pycache__/pprof_pb2.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__pycache__/pprof_pb2.cpython-38.pyc new file mode 100644 index 000000000..f63bcc055 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__pycache__/pprof_pb2.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__pycache__/pprof_pre312_pb2.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__pycache__/pprof_pre312_pb2.cpython-38.pyc new file mode 100644 index 000000000..abd506104 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/__pycache__/pprof_pre312_pb2.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/file.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/file.py new file mode 100644 index 000000000..6056db437 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/file.py @@ -0,0 +1,30 @@ +import gzip +import os + +import attr + +from ddtrace.profiling.exporter import pprof + + +@attr.s +class PprofFileExporter(pprof.PprofExporter): + """PProf file exporter.""" + + prefix = attr.ib() + _increment = attr.ib(default=1, init=False, repr=False) + + def export(self, events, start_time_ns, end_time_ns): + # type: (...) -> None + """Export events to pprof file. + + The file name is based on the prefix passed to init. The process ID number and type of export is then added as a + suffix. + + :param events: The event dictionary from a `ddtrace.profiling.recorder.Recorder`. + :param start_time_ns: The start time of recording. + :param end_time_ns: The end time of recording. + """ + profile = super(PprofFileExporter, self).export(events, start_time_ns, end_time_ns) + with gzip.open(self.prefix + (".%d.%d" % (os.getpid(), self._increment)), "wb") as f: + f.write(profile.SerializeToString()) + self._increment += 1 diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/http.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/http.py new file mode 100644 index 000000000..c1ac8b3cb --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/http.py @@ -0,0 +1,204 @@ +# -*- encoding: utf-8 -*- +import binascii +import datetime +import gzip +import itertools +import os +import platform + +import attr +import six +from six.moves import http_client +import tenacity + +import ddtrace +from ddtrace.internal import agent +from ddtrace.internal import runtime +from ddtrace.internal.runtime import container +from ddtrace.profiling import exporter +from ddtrace.profiling.exporter import pprof +from ddtrace.utils import attr as attr_utils +from ddtrace.utils.formats import parse_tags_str + + +HOSTNAME = platform.node() +PYTHON_IMPLEMENTATION = platform.python_implementation().encode() +PYTHON_VERSION = platform.python_version().encode() + + +class UploadFailed(tenacity.RetryError, exporter.ExportError): + """Upload failure.""" + + def __str__(self): + return str(self.last_attempt.exception()) + + +@attr.s +class PprofHTTPExporter(pprof.PprofExporter): + """PProf HTTP exporter.""" + + endpoint = attr.ib() + api_key = attr.ib(default=None) + # Do not use the default agent timeout: it is too short, the agent is just a unbuffered proxy and the profiling + # backend is not as fast as the tracer one. + timeout = attr.ib(factory=attr_utils.from_env("DD_PROFILING_API_TIMEOUT", 10.0, float), type=float) + service = attr.ib(default=None) + env = attr.ib(default=None) + version = attr.ib(default=None) + tags = attr.ib(factory=dict) + max_retry_delay = attr.ib(default=None) + _container_info = attr.ib(factory=container.get_container_info, repr=False) + _retry_upload = attr.ib(init=False, eq=False) + endpoint_path = attr.ib(default="/profiling/v1/input") + + def __attrs_post_init__(self): + if self.max_retry_delay is None: + self.max_retry_delay = self.timeout * 3 + self._retry_upload = tenacity.Retrying( + # Retry after 1s, 2s, 4s, 8s with some randomness + wait=tenacity.wait_random_exponential(multiplier=0.5), + stop=tenacity.stop_after_delay(self.max_retry_delay), + retry_error_cls=UploadFailed, + retry=tenacity.retry_if_exception_type((http_client.HTTPException, OSError, IOError)), + ) + tags = { + k: six.ensure_binary(v) + for k, v in itertools.chain( + parse_tags_str(os.environ.get("DD_TAGS")).items(), + parse_tags_str(os.environ.get("DD_PROFILING_TAGS")).items(), + ) + } + tags.update({k: six.ensure_binary(v) for k, v in self.tags.items()}) + tags.update( + { + "host": HOSTNAME.encode("utf-8"), + "language": b"python", + "runtime": PYTHON_IMPLEMENTATION, + "runtime_version": PYTHON_VERSION, + "profiler_version": ddtrace.__version__.encode("ascii"), + } + ) + if self.version: + tags["version"] = self.version.encode("utf-8") + + if self.env: + tags["env"] = self.env.encode("utf-8") + + self.tags = tags + + @staticmethod + def _encode_multipart_formdata(fields, tags): + boundary = binascii.hexlify(os.urandom(16)) + + # The body that is generated is very sensitive and must perfectly match what the server expects. + body = ( + b"".join( + b"--%s\r\n" + b'Content-Disposition: form-data; name="%s"\r\n' + b"\r\n" + b"%s\r\n" % (boundary, field.encode(), value) + for field, value in fields.items() + if field != "chunk-data" + ) + + b"".join( + b"--%s\r\n" + b'Content-Disposition: form-data; name="tags[]"\r\n' + b"\r\n" + b"%s:%s\r\n" % (boundary, tag.encode(), value) + for tag, value in tags.items() + ) + + b"--" + + boundary + + b"\r\n" + b'Content-Disposition: form-data; name="chunk-data"; filename="profile.pb.gz"\r\n' + + b"Content-Type: application/octet-stream\r\n\r\n" + + fields["chunk-data"] + + b"\r\n--%s--\r\n" % boundary + ) + + content_type = b"multipart/form-data; boundary=%s" % boundary + + return content_type, body + + def _get_tags(self, service): + tags = { + "service": service.encode("utf-8"), + "runtime-id": runtime.get_runtime_id().encode("ascii"), + } + + tags.update(self.tags) + + return tags + + def export(self, events, start_time_ns, end_time_ns): + """Export events to an HTTP endpoint. + + :param events: The event dictionary from a `ddtrace.profiling.recorder.Recorder`. + :param start_time_ns: The start time of recording. + :param end_time_ns: The end time of recording. + """ + if self.api_key: + headers = { + "DD-API-KEY": self.api_key.encode(), + } + else: + headers = {} + + if self._container_info and self._container_info.container_id: + headers["Datadog-Container-Id"] = self._container_info.container_id + + profile = super(PprofHTTPExporter, self).export(events, start_time_ns, end_time_ns) + s = six.BytesIO() + with gzip.GzipFile(fileobj=s, mode="wb") as gz: + gz.write(profile.SerializeToString()) + fields = { + "runtime-id": runtime.get_runtime_id().encode("ascii"), + "recording-start": ( + datetime.datetime.utcfromtimestamp(start_time_ns / 1e9).replace(microsecond=0).isoformat() + "Z" + ).encode(), + "recording-end": ( + datetime.datetime.utcfromtimestamp(end_time_ns / 1e9).replace(microsecond=0).isoformat() + "Z" + ).encode(), + "runtime": PYTHON_IMPLEMENTATION, + "format": b"pprof", + "type": b"cpu+alloc+exceptions", + "chunk-data": s.getvalue(), + } + + service = self.service or os.path.basename(profile.string_table[profile.mapping[0].filename]) + + content_type, body = self._encode_multipart_formdata( + fields, + tags=self._get_tags(service), + ) + headers["Content-Type"] = content_type + + client = agent.get_connection(self.endpoint, self.timeout) + self._upload(client, self.endpoint_path, body, headers) + + def _upload(self, client, path, body, headers): + self._retry_upload(self._upload_once, client, path, body, headers) + + def _upload_once(self, client, path, body, headers): + try: + client.request("POST", path, body=body, headers=headers) + response = client.getresponse() + response.read() # reading is mandatory + finally: + client.close() + + if 200 <= response.status < 300: + return + + if 500 <= response.status < 600: + raise tenacity.TryAgain + + if response.status == 400: + raise exporter.ExportError("Server returned 400, check your API key") + elif response.status == 404 and not self.api_key: + raise exporter.ExportError( + "Datadog Agent is not accepting profiles. " + "Agent-based profiling deployments require Datadog Agent >= 7.20" + ) + + raise exporter.ExportError("HTTP Error %d" % response.status) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/pprof.cpython-38-darwin.so b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/pprof.cpython-38-darwin.so new file mode 100755 index 000000000..8d8a86cd3 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/pprof.cpython-38-darwin.so differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/pprof_pb2.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/pprof_pb2.py new file mode 100644 index 000000000..5cd24fe74 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/pprof_pb2.py @@ -0,0 +1,621 @@ +# ignore typing until is released https://github.com/python/typeshed/pull/5007 +# type: ignore + +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: ddtrace/profiling/exporter/pprof.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='ddtrace/profiling/exporter/pprof.proto', + package='perftools.profiles', + syntax='proto3', + serialized_options=b'\n\035com.google.perftools.profilesB\014ProfileProto', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n&ddtrace/profiling/exporter/pprof.proto\x12\x12perftools.profiles\"\xd5\x03\n\x07Profile\x12\x32\n\x0bsample_type\x18\x01 \x03(\x0b\x32\x1d.perftools.profiles.ValueType\x12*\n\x06sample\x18\x02 \x03(\x0b\x32\x1a.perftools.profiles.Sample\x12,\n\x07mapping\x18\x03 \x03(\x0b\x32\x1b.perftools.profiles.Mapping\x12.\n\x08location\x18\x04 \x03(\x0b\x32\x1c.perftools.profiles.Location\x12.\n\x08\x66unction\x18\x05 \x03(\x0b\x32\x1c.perftools.profiles.Function\x12\x14\n\x0cstring_table\x18\x06 \x03(\t\x12\x13\n\x0b\x64rop_frames\x18\x07 \x01(\x03\x12\x13\n\x0bkeep_frames\x18\x08 \x01(\x03\x12\x12\n\ntime_nanos\x18\t \x01(\x03\x12\x16\n\x0e\x64uration_nanos\x18\n \x01(\x03\x12\x32\n\x0bperiod_type\x18\x0b \x01(\x0b\x32\x1d.perftools.profiles.ValueType\x12\x0e\n\x06period\x18\x0c \x01(\x03\x12\x0f\n\x07\x63omment\x18\r \x03(\x03\x12\x1b\n\x13\x64\x65\x66\x61ult_sample_type\x18\x0e \x01(\x03\"\'\n\tValueType\x12\x0c\n\x04type\x18\x01 \x01(\x03\x12\x0c\n\x04unit\x18\x02 \x01(\x03\"V\n\x06Sample\x12\x13\n\x0blocation_id\x18\x01 \x03(\x04\x12\r\n\x05value\x18\x02 \x03(\x03\x12(\n\x05label\x18\x03 \x03(\x0b\x32\x19.perftools.profiles.Label\"@\n\x05Label\x12\x0b\n\x03key\x18\x01 \x01(\x03\x12\x0b\n\x03str\x18\x02 \x01(\x03\x12\x0b\n\x03num\x18\x03 \x01(\x03\x12\x10\n\x08num_unit\x18\x04 \x01(\x03\"\xdd\x01\n\x07Mapping\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x14\n\x0cmemory_start\x18\x02 \x01(\x04\x12\x14\n\x0cmemory_limit\x18\x03 \x01(\x04\x12\x13\n\x0b\x66ile_offset\x18\x04 \x01(\x04\x12\x10\n\x08\x66ilename\x18\x05 \x01(\x03\x12\x10\n\x08\x62uild_id\x18\x06 \x01(\x03\x12\x15\n\rhas_functions\x18\x07 \x01(\x08\x12\x15\n\rhas_filenames\x18\x08 \x01(\x08\x12\x18\n\x10has_line_numbers\x18\t \x01(\x08\x12\x19\n\x11has_inline_frames\x18\n \x01(\x08\"v\n\x08Location\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x12\n\nmapping_id\x18\x02 \x01(\x04\x12\x0f\n\x07\x61\x64\x64ress\x18\x03 \x01(\x04\x12&\n\x04line\x18\x04 \x03(\x0b\x32\x18.perftools.profiles.Line\x12\x11\n\tis_folded\x18\x05 \x01(\x08\")\n\x04Line\x12\x13\n\x0b\x66unction_id\x18\x01 \x01(\x04\x12\x0c\n\x04line\x18\x02 \x01(\x03\"_\n\x08\x46unction\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\x03\x12\x13\n\x0bsystem_name\x18\x03 \x01(\x03\x12\x10\n\x08\x66ilename\x18\x04 \x01(\x03\x12\x12\n\nstart_line\x18\x05 \x01(\x03\x42-\n\x1d\x63om.google.perftools.profilesB\x0cProfileProtob\x06proto3' +) + + + + +_PROFILE = _descriptor.Descriptor( + name='Profile', + full_name='perftools.profiles.Profile', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='sample_type', full_name='perftools.profiles.Profile.sample_type', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='sample', full_name='perftools.profiles.Profile.sample', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='mapping', full_name='perftools.profiles.Profile.mapping', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='location', full_name='perftools.profiles.Profile.location', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='function', full_name='perftools.profiles.Profile.function', index=4, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='string_table', full_name='perftools.profiles.Profile.string_table', index=5, + number=6, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='drop_frames', full_name='perftools.profiles.Profile.drop_frames', index=6, + number=7, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='keep_frames', full_name='perftools.profiles.Profile.keep_frames', index=7, + number=8, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='time_nanos', full_name='perftools.profiles.Profile.time_nanos', index=8, + number=9, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='duration_nanos', full_name='perftools.profiles.Profile.duration_nanos', index=9, + number=10, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='period_type', full_name='perftools.profiles.Profile.period_type', index=10, + number=11, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='period', full_name='perftools.profiles.Profile.period', index=11, + number=12, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='comment', full_name='perftools.profiles.Profile.comment', index=12, + number=13, type=3, cpp_type=2, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='default_sample_type', full_name='perftools.profiles.Profile.default_sample_type', index=13, + number=14, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=63, + serialized_end=532, +) + + +_VALUETYPE = _descriptor.Descriptor( + name='ValueType', + full_name='perftools.profiles.ValueType', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='type', full_name='perftools.profiles.ValueType.type', index=0, + number=1, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='unit', full_name='perftools.profiles.ValueType.unit', index=1, + number=2, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=534, + serialized_end=573, +) + + +_SAMPLE = _descriptor.Descriptor( + name='Sample', + full_name='perftools.profiles.Sample', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='location_id', full_name='perftools.profiles.Sample.location_id', index=0, + number=1, type=4, cpp_type=4, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='value', full_name='perftools.profiles.Sample.value', index=1, + number=2, type=3, cpp_type=2, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='label', full_name='perftools.profiles.Sample.label', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=575, + serialized_end=661, +) + + +_LABEL = _descriptor.Descriptor( + name='Label', + full_name='perftools.profiles.Label', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='perftools.profiles.Label.key', index=0, + number=1, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='str', full_name='perftools.profiles.Label.str', index=1, + number=2, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='num', full_name='perftools.profiles.Label.num', index=2, + number=3, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='num_unit', full_name='perftools.profiles.Label.num_unit', index=3, + number=4, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=663, + serialized_end=727, +) + + +_MAPPING = _descriptor.Descriptor( + name='Mapping', + full_name='perftools.profiles.Mapping', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='id', full_name='perftools.profiles.Mapping.id', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='memory_start', full_name='perftools.profiles.Mapping.memory_start', index=1, + number=2, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='memory_limit', full_name='perftools.profiles.Mapping.memory_limit', index=2, + number=3, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='file_offset', full_name='perftools.profiles.Mapping.file_offset', index=3, + number=4, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='filename', full_name='perftools.profiles.Mapping.filename', index=4, + number=5, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='build_id', full_name='perftools.profiles.Mapping.build_id', index=5, + number=6, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='has_functions', full_name='perftools.profiles.Mapping.has_functions', index=6, + number=7, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='has_filenames', full_name='perftools.profiles.Mapping.has_filenames', index=7, + number=8, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='has_line_numbers', full_name='perftools.profiles.Mapping.has_line_numbers', index=8, + number=9, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='has_inline_frames', full_name='perftools.profiles.Mapping.has_inline_frames', index=9, + number=10, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=730, + serialized_end=951, +) + + +_LOCATION = _descriptor.Descriptor( + name='Location', + full_name='perftools.profiles.Location', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='id', full_name='perftools.profiles.Location.id', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='mapping_id', full_name='perftools.profiles.Location.mapping_id', index=1, + number=2, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='address', full_name='perftools.profiles.Location.address', index=2, + number=3, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='line', full_name='perftools.profiles.Location.line', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='is_folded', full_name='perftools.profiles.Location.is_folded', index=4, + number=5, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=953, + serialized_end=1071, +) + + +_LINE = _descriptor.Descriptor( + name='Line', + full_name='perftools.profiles.Line', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='function_id', full_name='perftools.profiles.Line.function_id', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='line', full_name='perftools.profiles.Line.line', index=1, + number=2, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1073, + serialized_end=1114, +) + + +_FUNCTION = _descriptor.Descriptor( + name='Function', + full_name='perftools.profiles.Function', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='id', full_name='perftools.profiles.Function.id', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='name', full_name='perftools.profiles.Function.name', index=1, + number=2, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='system_name', full_name='perftools.profiles.Function.system_name', index=2, + number=3, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='filename', full_name='perftools.profiles.Function.filename', index=3, + number=4, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='start_line', full_name='perftools.profiles.Function.start_line', index=4, + number=5, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1116, + serialized_end=1211, +) + +_PROFILE.fields_by_name['sample_type'].message_type = _VALUETYPE +_PROFILE.fields_by_name['sample'].message_type = _SAMPLE +_PROFILE.fields_by_name['mapping'].message_type = _MAPPING +_PROFILE.fields_by_name['location'].message_type = _LOCATION +_PROFILE.fields_by_name['function'].message_type = _FUNCTION +_PROFILE.fields_by_name['period_type'].message_type = _VALUETYPE +_SAMPLE.fields_by_name['label'].message_type = _LABEL +_LOCATION.fields_by_name['line'].message_type = _LINE +DESCRIPTOR.message_types_by_name['Profile'] = _PROFILE +DESCRIPTOR.message_types_by_name['ValueType'] = _VALUETYPE +DESCRIPTOR.message_types_by_name['Sample'] = _SAMPLE +DESCRIPTOR.message_types_by_name['Label'] = _LABEL +DESCRIPTOR.message_types_by_name['Mapping'] = _MAPPING +DESCRIPTOR.message_types_by_name['Location'] = _LOCATION +DESCRIPTOR.message_types_by_name['Line'] = _LINE +DESCRIPTOR.message_types_by_name['Function'] = _FUNCTION +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Profile = _reflection.GeneratedProtocolMessageType('Profile', (_message.Message,), { + 'DESCRIPTOR' : _PROFILE, + '__module__' : 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.Profile) + }) +_sym_db.RegisterMessage(Profile) + +ValueType = _reflection.GeneratedProtocolMessageType('ValueType', (_message.Message,), { + 'DESCRIPTOR' : _VALUETYPE, + '__module__' : 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.ValueType) + }) +_sym_db.RegisterMessage(ValueType) + +Sample = _reflection.GeneratedProtocolMessageType('Sample', (_message.Message,), { + 'DESCRIPTOR' : _SAMPLE, + '__module__' : 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.Sample) + }) +_sym_db.RegisterMessage(Sample) + +Label = _reflection.GeneratedProtocolMessageType('Label', (_message.Message,), { + 'DESCRIPTOR' : _LABEL, + '__module__' : 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.Label) + }) +_sym_db.RegisterMessage(Label) + +Mapping = _reflection.GeneratedProtocolMessageType('Mapping', (_message.Message,), { + 'DESCRIPTOR' : _MAPPING, + '__module__' : 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.Mapping) + }) +_sym_db.RegisterMessage(Mapping) + +Location = _reflection.GeneratedProtocolMessageType('Location', (_message.Message,), { + 'DESCRIPTOR' : _LOCATION, + '__module__' : 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.Location) + }) +_sym_db.RegisterMessage(Location) + +Line = _reflection.GeneratedProtocolMessageType('Line', (_message.Message,), { + 'DESCRIPTOR' : _LINE, + '__module__' : 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.Line) + }) +_sym_db.RegisterMessage(Line) + +Function = _reflection.GeneratedProtocolMessageType('Function', (_message.Message,), { + 'DESCRIPTOR' : _FUNCTION, + '__module__' : 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.Function) + }) +_sym_db.RegisterMessage(Function) + + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/pprof_pre312_pb2.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/pprof_pre312_pb2.py new file mode 100644 index 000000000..159117429 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/exporter/pprof_pre312_pb2.py @@ -0,0 +1,615 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: ddtrace/profiling/exporter/pprof.proto + +import sys + + +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pb2 +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='ddtrace.profiling/exporter/pprof.proto', + package='perftools.profiles', + syntax='proto3', + serialized_pb=_b('\n\x1e\x64\x64profile/exporter/pprof.proto\x12\x12perftools.profiles\"\xd5\x03\n\x07Profile\x12\x32\n\x0bsample_type\x18\x01 \x03(\x0b\x32\x1d.perftools.profiles.ValueType\x12*\n\x06sample\x18\x02 \x03(\x0b\x32\x1a.perftools.profiles.Sample\x12,\n\x07mapping\x18\x03 \x03(\x0b\x32\x1b.perftools.profiles.Mapping\x12.\n\x08location\x18\x04 \x03(\x0b\x32\x1c.perftools.profiles.Location\x12.\n\x08\x66unction\x18\x05 \x03(\x0b\x32\x1c.perftools.profiles.Function\x12\x14\n\x0cstring_table\x18\x06 \x03(\t\x12\x13\n\x0b\x64rop_frames\x18\x07 \x01(\x03\x12\x13\n\x0bkeep_frames\x18\x08 \x01(\x03\x12\x12\n\ntime_nanos\x18\t \x01(\x03\x12\x16\n\x0e\x64uration_nanos\x18\n \x01(\x03\x12\x32\n\x0bperiod_type\x18\x0b \x01(\x0b\x32\x1d.perftools.profiles.ValueType\x12\x0e\n\x06period\x18\x0c \x01(\x03\x12\x0f\n\x07\x63omment\x18\r \x03(\x03\x12\x1b\n\x13\x64\x65\x66\x61ult_sample_type\x18\x0e \x01(\x03\"\'\n\tValueType\x12\x0c\n\x04type\x18\x01 \x01(\x03\x12\x0c\n\x04unit\x18\x02 \x01(\x03\"V\n\x06Sample\x12\x13\n\x0blocation_id\x18\x01 \x03(\x04\x12\r\n\x05value\x18\x02 \x03(\x03\x12(\n\x05label\x18\x03 \x03(\x0b\x32\x19.perftools.profiles.Label\"@\n\x05Label\x12\x0b\n\x03key\x18\x01 \x01(\x03\x12\x0b\n\x03str\x18\x02 \x01(\x03\x12\x0b\n\x03num\x18\x03 \x01(\x03\x12\x10\n\x08num_unit\x18\x04 \x01(\x03\"\xdd\x01\n\x07Mapping\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x14\n\x0cmemory_start\x18\x02 \x01(\x04\x12\x14\n\x0cmemory_limit\x18\x03 \x01(\x04\x12\x13\n\x0b\x66ile_offset\x18\x04 \x01(\x04\x12\x10\n\x08\x66ilename\x18\x05 \x01(\x03\x12\x10\n\x08\x62uild_id\x18\x06 \x01(\x03\x12\x15\n\rhas_functions\x18\x07 \x01(\x08\x12\x15\n\rhas_filenames\x18\x08 \x01(\x08\x12\x18\n\x10has_line_numbers\x18\t \x01(\x08\x12\x19\n\x11has_inline_frames\x18\n \x01(\x08\"v\n\x08Location\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x12\n\nmapping_id\x18\x02 \x01(\x04\x12\x0f\n\x07\x61\x64\x64ress\x18\x03 \x01(\x04\x12&\n\x04line\x18\x04 \x03(\x0b\x32\x18.perftools.profiles.Line\x12\x11\n\tis_folded\x18\x05 \x01(\x08\")\n\x04Line\x12\x13\n\x0b\x66unction_id\x18\x01 \x01(\x04\x12\x0c\n\x04line\x18\x02 \x01(\x03\"_\n\x08\x46unction\x12\n\n\x02id\x18\x01 \x01(\x04\x12\x0c\n\x04name\x18\x02 \x01(\x03\x12\x13\n\x0bsystem_name\x18\x03 \x01(\x03\x12\x10\n\x08\x66ilename\x18\x04 \x01(\x03\x12\x12\n\nstart_line\x18\x05 \x01(\x03\x42-\n\x1d\x63om.google.perftools.profilesB\x0cProfileProtob\x06proto3') +) +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + + + + +_PROFILE = _descriptor.Descriptor( + name='Profile', + full_name='perftools.profiles.Profile', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='sample_type', full_name='perftools.profiles.Profile.sample_type', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='sample', full_name='perftools.profiles.Profile.sample', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='mapping', full_name='perftools.profiles.Profile.mapping', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='location', full_name='perftools.profiles.Profile.location', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='function', full_name='perftools.profiles.Profile.function', index=4, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='string_table', full_name='perftools.profiles.Profile.string_table', index=5, + number=6, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='drop_frames', full_name='perftools.profiles.Profile.drop_frames', index=6, + number=7, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='keep_frames', full_name='perftools.profiles.Profile.keep_frames', index=7, + number=8, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='time_nanos', full_name='perftools.profiles.Profile.time_nanos', index=8, + number=9, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='duration_nanos', full_name='perftools.profiles.Profile.duration_nanos', index=9, + number=10, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='period_type', full_name='perftools.profiles.Profile.period_type', index=10, + number=11, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='period', full_name='perftools.profiles.Profile.period', index=11, + number=12, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='comment', full_name='perftools.profiles.Profile.comment', index=12, + number=13, type=3, cpp_type=2, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='default_sample_type', full_name='perftools.profiles.Profile.default_sample_type', index=13, + number=14, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=55, + serialized_end=524, +) + + +_VALUETYPE = _descriptor.Descriptor( + name='ValueType', + full_name='perftools.profiles.ValueType', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='type', full_name='perftools.profiles.ValueType.type', index=0, + number=1, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='unit', full_name='perftools.profiles.ValueType.unit', index=1, + number=2, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=526, + serialized_end=565, +) + + +_SAMPLE = _descriptor.Descriptor( + name='Sample', + full_name='perftools.profiles.Sample', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='location_id', full_name='perftools.profiles.Sample.location_id', index=0, + number=1, type=4, cpp_type=4, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='value', full_name='perftools.profiles.Sample.value', index=1, + number=2, type=3, cpp_type=2, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='label', full_name='perftools.profiles.Sample.label', index=2, + number=3, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=567, + serialized_end=653, +) + + +_LABEL = _descriptor.Descriptor( + name='Label', + full_name='perftools.profiles.Label', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key', full_name='perftools.profiles.Label.key', index=0, + number=1, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='str', full_name='perftools.profiles.Label.str', index=1, + number=2, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='num', full_name='perftools.profiles.Label.num', index=2, + number=3, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='num_unit', full_name='perftools.profiles.Label.num_unit', index=3, + number=4, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=655, + serialized_end=719, +) + + +_MAPPING = _descriptor.Descriptor( + name='Mapping', + full_name='perftools.profiles.Mapping', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='id', full_name='perftools.profiles.Mapping.id', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='memory_start', full_name='perftools.profiles.Mapping.memory_start', index=1, + number=2, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='memory_limit', full_name='perftools.profiles.Mapping.memory_limit', index=2, + number=3, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='file_offset', full_name='perftools.profiles.Mapping.file_offset', index=3, + number=4, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='filename', full_name='perftools.profiles.Mapping.filename', index=4, + number=5, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='build_id', full_name='perftools.profiles.Mapping.build_id', index=5, + number=6, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='has_functions', full_name='perftools.profiles.Mapping.has_functions', index=6, + number=7, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='has_filenames', full_name='perftools.profiles.Mapping.has_filenames', index=7, + number=8, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='has_line_numbers', full_name='perftools.profiles.Mapping.has_line_numbers', index=8, + number=9, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='has_inline_frames', full_name='perftools.profiles.Mapping.has_inline_frames', index=9, + number=10, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=722, + serialized_end=943, +) + + +_LOCATION = _descriptor.Descriptor( + name='Location', + full_name='perftools.profiles.Location', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='id', full_name='perftools.profiles.Location.id', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='mapping_id', full_name='perftools.profiles.Location.mapping_id', index=1, + number=2, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='address', full_name='perftools.profiles.Location.address', index=2, + number=3, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='line', full_name='perftools.profiles.Location.line', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='is_folded', full_name='perftools.profiles.Location.is_folded', index=4, + number=5, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=945, + serialized_end=1063, +) + + +_LINE = _descriptor.Descriptor( + name='Line', + full_name='perftools.profiles.Line', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='function_id', full_name='perftools.profiles.Line.function_id', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='line', full_name='perftools.profiles.Line.line', index=1, + number=2, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1065, + serialized_end=1106, +) + + +_FUNCTION = _descriptor.Descriptor( + name='Function', + full_name='perftools.profiles.Function', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='id', full_name='perftools.profiles.Function.id', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='name', full_name='perftools.profiles.Function.name', index=1, + number=2, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='system_name', full_name='perftools.profiles.Function.system_name', index=2, + number=3, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='filename', full_name='perftools.profiles.Function.filename', index=3, + number=4, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='start_line', full_name='perftools.profiles.Function.start_line', index=4, + number=5, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1108, + serialized_end=1203, +) + +_PROFILE.fields_by_name['sample_type'].message_type = _VALUETYPE +_PROFILE.fields_by_name['sample'].message_type = _SAMPLE +_PROFILE.fields_by_name['mapping'].message_type = _MAPPING +_PROFILE.fields_by_name['location'].message_type = _LOCATION +_PROFILE.fields_by_name['function'].message_type = _FUNCTION +_PROFILE.fields_by_name['period_type'].message_type = _VALUETYPE +_SAMPLE.fields_by_name['label'].message_type = _LABEL +_LOCATION.fields_by_name['line'].message_type = _LINE +DESCRIPTOR.message_types_by_name['Profile'] = _PROFILE +DESCRIPTOR.message_types_by_name['ValueType'] = _VALUETYPE +DESCRIPTOR.message_types_by_name['Sample'] = _SAMPLE +DESCRIPTOR.message_types_by_name['Label'] = _LABEL +DESCRIPTOR.message_types_by_name['Mapping'] = _MAPPING +DESCRIPTOR.message_types_by_name['Location'] = _LOCATION +DESCRIPTOR.message_types_by_name['Line'] = _LINE +DESCRIPTOR.message_types_by_name['Function'] = _FUNCTION + +Profile = _reflection.GeneratedProtocolMessageType('Profile', (_message.Message,), dict( + DESCRIPTOR = _PROFILE, + __module__ = 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.Profile) + )) +_sym_db.RegisterMessage(Profile) + +ValueType = _reflection.GeneratedProtocolMessageType('ValueType', (_message.Message,), dict( + DESCRIPTOR = _VALUETYPE, + __module__ = 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.ValueType) + )) +_sym_db.RegisterMessage(ValueType) + +Sample = _reflection.GeneratedProtocolMessageType('Sample', (_message.Message,), dict( + DESCRIPTOR = _SAMPLE, + __module__ = 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.Sample) + )) +_sym_db.RegisterMessage(Sample) + +Label = _reflection.GeneratedProtocolMessageType('Label', (_message.Message,), dict( + DESCRIPTOR = _LABEL, + __module__ = 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.Label) + )) +_sym_db.RegisterMessage(Label) + +Mapping = _reflection.GeneratedProtocolMessageType('Mapping', (_message.Message,), dict( + DESCRIPTOR = _MAPPING, + __module__ = 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.Mapping) + )) +_sym_db.RegisterMessage(Mapping) + +Location = _reflection.GeneratedProtocolMessageType('Location', (_message.Message,), dict( + DESCRIPTOR = _LOCATION, + __module__ = 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.Location) + )) +_sym_db.RegisterMessage(Location) + +Line = _reflection.GeneratedProtocolMessageType('Line', (_message.Message,), dict( + DESCRIPTOR = _LINE, + __module__ = 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.Line) + )) +_sym_db.RegisterMessage(Line) + +Function = _reflection.GeneratedProtocolMessageType('Function', (_message.Message,), dict( + DESCRIPTOR = _FUNCTION, + __module__ = 'ddtrace.profiling.exporter.pprof_pb2' + # @@protoc_insertion_point(class_scope:perftools.profiles.Function) + )) +_sym_db.RegisterMessage(Function) + + +DESCRIPTOR.has_options = True +DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n\035com.google.perftools.profilesB\014ProfileProto')) +# @@protoc_insertion_point(module_scope) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/profiler.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/profiler.py new file mode 100644 index 000000000..028875014 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/profiler.py @@ -0,0 +1,281 @@ +# -*- encoding: utf-8 -*- +import logging +import os +from typing import List +from typing import Optional + +import attr + +import ddtrace +from ddtrace.internal import agent +from ddtrace.internal import atexit +from ddtrace.internal import service +from ddtrace.internal import uwsgi +from ddtrace.internal import writer +from ddtrace.profiling import collector +from ddtrace.profiling import exporter +from ddtrace.profiling import recorder +from ddtrace.profiling import scheduler +from ddtrace.profiling.collector import memalloc +from ddtrace.profiling.collector import stack +from ddtrace.profiling.collector import threading +from ddtrace.profiling.exporter import file +from ddtrace.profiling.exporter import http +from ddtrace.utils import formats + + +LOG = logging.getLogger(__name__) + + +def _get_service_name(): + for service_name_var in ("DD_SERVICE", "DD_SERVICE_NAME", "DATADOG_SERVICE_NAME"): + service_name = os.environ.get(service_name_var) + if service_name is not None: + return service_name + + +class Profiler(object): + """Run profiling while code is executed. + + Note that the whole Python process is profiled, not only the code executed. Data from all running threads are + caught. + + """ + + def __init__(self, *args, **kwargs): + self._profiler = _ProfilerInstance(*args, **kwargs) + + def start(self, stop_on_exit=True, profile_children=True): + """Start the profiler. + + :param stop_on_exit: Whether to stop the profiler and flush the profile on exit. + :param profile_children: Whether to start a profiler in child processes. + """ + + if profile_children: + try: + uwsgi.check_uwsgi(self._restart_on_fork, atexit=self.stop if stop_on_exit else None) + except uwsgi.uWSGIMasterProcess: + # Do nothing, the start() method will be called in each worker subprocess + return + + self._profiler.start() + + if stop_on_exit: + atexit.register(self.stop) + + if profile_children: + if hasattr(os, "register_at_fork"): + os.register_at_fork(after_in_child=self._restart_on_fork) + else: + LOG.warning( + "Your Python version does not have `os.register_at_fork`. " + "You have to start a new Profiler after fork() manually." + ) + + def stop(self, flush=True): + """Stop the profiler. + + :param flush: Flush last profile. + """ + atexit.unregister(self.stop) + try: + self._profiler.stop(flush) + except service.ServiceStatusError: + # Not a best practice, but for backward API compatibility that allowed to call `stop` multiple times. + pass + + def _restart_on_fork(self): + # Be sure to stop the parent first, since it might have to e.g. unpatch functions + # Do not flush data as we don't want to have multiple copies of the parent profile exported. + try: + self._profiler.stop(flush=False) + except service.ServiceStatusError: + # This can happen in uWSGI mode: the children won't have the _profiler started from the master process + pass + self._profiler = self._profiler.copy() + self._profiler.start() + + @property + def status(self): + return self._profiler.status + + @property + def service(self): + return self._profiler.service + + @property + def env(self): + return self._profiler.env + + @property + def version(self): + return self._profiler.version + + @property + def tracer(self): + return self._profiler.tracer + + @property + def url(self): + return self._profiler.url + + @property + def tags(self): + return self._profiler.tags + + +@attr.s +class _ProfilerInstance(service.Service): + """A instance of the profiler. + + Each process must manage its own instance. + + """ + + # User-supplied values + url = attr.ib(default=None) + service = attr.ib(factory=_get_service_name) + tags = attr.ib(factory=dict) + env = attr.ib(factory=lambda: os.environ.get("DD_ENV")) + version = attr.ib(factory=lambda: os.environ.get("DD_VERSION")) + tracer = attr.ib(default=ddtrace.tracer) + api_key = attr.ib(factory=lambda: os.environ.get("DD_API_KEY"), type=Optional[str]) + agentless = attr.ib(factory=lambda: formats.asbool(os.environ.get("DD_PROFILING_AGENTLESS", "False")), type=bool) + + _recorder = attr.ib(init=False, default=None) + _collectors = attr.ib(init=False, default=None) + _scheduler = attr.ib(init=False, default=None) + + ENDPOINT_TEMPLATE = "https://intake.profile.{}" + + def _build_default_exporters(self): + # type: (...) -> List[exporter.Exporter] + _OUTPUT_PPROF = os.environ.get("DD_PROFILING_OUTPUT_PPROF") + if _OUTPUT_PPROF: + return [ + file.PprofFileExporter(_OUTPUT_PPROF), + ] + + if self.url is not None: + endpoint = self.url + elif self.agentless: + LOG.warning( + "Agentless uploading is currently for internal usage only and not officially supported. " + "You should not enable it unless somebody at Datadog instructed you to do so." + ) + endpoint = self.ENDPOINT_TEMPLATE.format(os.environ.get("DD_SITE", "datadoghq.com")) + else: + if isinstance(self.tracer.writer, writer.AgentWriter): + endpoint = self.tracer.writer.agent_url + else: + endpoint = agent.get_trace_url() + + if self.agentless: + endpoint_path = "/v1/input" + else: + # Agent mode + # path is relative because it is appended + # to the agent base path. + endpoint_path = "profiling/v1/input" + + return [ + http.PprofHTTPExporter( + service=self.service, + env=self.env, + tags=self.tags, + version=self.version, + api_key=self.api_key, + endpoint=endpoint, + endpoint_path=endpoint_path, + ), + ] + + def __attrs_post_init__(self): + r = self._recorder = recorder.Recorder( + max_events={ + # Allow to store up to 10 threads for 60 seconds at 100 Hz + stack.StackSampleEvent: 10 * 60 * 100, + stack.StackExceptionSampleEvent: 10 * 60 * 100, + # (default buffer size / interval) * export interval + memalloc.MemoryAllocSampleEvent: int( + (memalloc.MemoryCollector._DEFAULT_MAX_EVENTS / memalloc.MemoryCollector._DEFAULT_INTERVAL) * 60 + ), + # Do not limit the heap sample size as the number of events is relative to allocated memory anyway + memalloc.MemoryHeapSampleEvent: None, + }, + default_max_events=int(os.environ.get("DD_PROFILING_MAX_EVENTS", recorder.Recorder._DEFAULT_MAX_EVENTS)), + ) + + self._collectors = [ + stack.StackCollector(r, tracer=self.tracer), + memalloc.MemoryCollector(r), + threading.LockCollector(r, tracer=self.tracer), + ] + + exporters = self._build_default_exporters() + + if exporters: + self._scheduler = scheduler.Scheduler( + recorder=r, exporters=exporters, before_flush=self._collectors_snapshot + ) + + def _collectors_snapshot(self): + for c in self._collectors: + try: + snapshot = c.snapshot() + if snapshot: + for events in snapshot: + self._recorder.push_events(events) + except Exception: + LOG.error("Error while snapshoting collector %r", c, exc_info=True) + + def copy(self): + return self.__class__( + service=self.service, env=self.env, version=self.version, tracer=self.tracer, tags=self.tags + ) + + def _start_service(self): # type: ignore[override] + # type: (...) -> None + """Start the profiler.""" + collectors = [] + for col in self._collectors: + try: + col.start() + except collector.CollectorUnavailable: + LOG.debug("Collector %r is unavailable, disabling", col) + except Exception: + LOG.error("Failed to start collector %r, disabling.", col, exc_info=True) + else: + collectors.append(col) + self._collectors = collectors + + if self._scheduler is not None: + self._scheduler.start() + + def _stop_service( # type: ignore[override] + self, flush=True # type: bool + ): + # type: (...) -> None + """Stop the profiler. + + :param flush: Flush a last profile. + """ + if self._scheduler is not None: + self._scheduler.stop() + # Wait for the export to be over: export might need collectors (e.g., for snapshot) so we can't stop + # collectors before the possibly running flush is finished. + self._scheduler.join() + if flush: + # Do not stop the collectors before flushing, they might be needed (snapshot) + self._scheduler.flush() + + for col in reversed(self._collectors): + try: + col.stop() + except service.ServiceStatusError: + # It's possible some collector failed to start, ignore failure to stop + pass + + for col in reversed(self._collectors): + col.join() diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/recorder.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/recorder.py new file mode 100644 index 000000000..1aa945818 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/recorder.py @@ -0,0 +1,90 @@ +# -*- encoding: utf-8 -*- +import collections + +import attr + +from ddtrace.internal import forksafe +from ddtrace.internal import nogevent + + +class _defaultdictkey(dict): + """A variant of defaultdict that calls default_factory with the missing key as argument.""" + + def __init__(self, default_factory=None): + self.default_factory = default_factory + + def __missing__(self, key): + if self.default_factory: + v = self[key] = self.default_factory(key) + return v + raise KeyError(key) + + +@attr.s +class Recorder(object): + """An object that records program activity.""" + + _DEFAULT_MAX_EVENTS = 32768 + + default_max_events = attr.ib(default=_DEFAULT_MAX_EVENTS) + """The maximum number of events for an event type if one is not specified.""" + + max_events = attr.ib(factory=dict) + """A dict of {event_type_class: max events} to limit the number of events to record.""" + + events = attr.ib(init=False, repr=False, eq=False) + _events_lock = attr.ib(init=False, repr=False, factory=nogevent.DoubleLock, eq=False) + + def __attrs_post_init__(self): + # type: (...) -> None + self._reset_events() + forksafe.register(self._after_fork) + + def _after_fork(self): + # type: (...) -> None + # NOTE: do not try to push events if the process forked + # This means we don't know the state of _events_lock and it might be unusable — we'd deadlock + self.push_events = self._push_events_noop # type: ignore[assignment] + + def _push_events_noop(self, events): + pass + + def push_event(self, event): + """Push an event in the recorder. + + :param event: The `ddtrace.profiling.event.Event` to push. + """ + return self.push_events([event]) + + def push_events(self, events): + """Push multiple events in the recorder. + + All the events MUST be of the same type. + There is no sanity check as whether all the events are from the same class for performance reasons. + + :param events: The event list to push. + """ + if events: + event_type = events[0].__class__ + with self._events_lock: + q = self.events[event_type] + q.extend(events) + + def _get_deque_for_event_type(self, event_type): + return collections.deque(maxlen=self.max_events.get(event_type, self.default_max_events)) + + def _reset_events(self): + self.events = _defaultdictkey(self._get_deque_for_event_type) + + def reset(self): + """Reset the recorder. + + This is useful when e.g. exporting data. Once the event queue is retrieved, a new one can be created by calling + the reset method, avoiding iterating on a mutating event list. + + :return: The list of events that has been removed. + """ + with self._events_lock: + events = self.events + self._reset_events() + return events diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/scheduler.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/scheduler.py new file mode 100644 index 000000000..bcde34761 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/profiling/scheduler.py @@ -0,0 +1,67 @@ +# -*- encoding: utf-8 -*- +import logging + +import attr + +from ddtrace.internal import compat +from ddtrace.internal import periodic +from ddtrace.profiling import _traceback +from ddtrace.profiling import exporter +from ddtrace.utils import attr as attr_utils + + +LOG = logging.getLogger(__name__) + + +@attr.s +class Scheduler(periodic.PeriodicService): + """Schedule export of recorded data.""" + + recorder = attr.ib() + exporters = attr.ib() + before_flush = attr.ib(default=None, eq=False) + _interval = attr.ib(factory=attr_utils.from_env("DD_PROFILING_UPLOAD_INTERVAL", 60.0, float)) + _configured_interval = attr.ib(init=False) + _last_export = attr.ib(init=False, default=None, eq=False) + + def __attrs_post_init__(self): + # Copy the value to use it later since we're going to adjust the real interval + self._configured_interval = self.interval + + def _start_service(self): # type: ignore[override] + # type: (...) -> None + """Start the scheduler.""" + LOG.debug("Starting scheduler") + super(Scheduler, self)._start_service() + self._last_export = compat.time_ns() + LOG.debug("Scheduler started") + + def flush(self): + """Flush events from recorder to exporters.""" + LOG.debug("Flushing events") + if self.before_flush is not None: + try: + self.before_flush() + except Exception: + LOG.error("Scheduler before_flush hook failed", exc_info=True) + if self.exporters: + events = self.recorder.reset() + start = self._last_export + self._last_export = compat.time_ns() + for exp in self.exporters: + try: + exp.export(events, start, self._last_export) + except exporter.ExportError as e: + LOG.error("Unable to export profile: %s. Ignoring.", _traceback.format_exception(e)) + except Exception: + LOG.exception( + "Unexpected error while exporting events. " + "Please report this bug to https://github.com/DataDog/dd-trace-py/issues" + ) + + def periodic(self): + start_time = compat.monotonic() + try: + self.flush() + finally: + self.interval = max(0, self._configured_interval - (compat.monotonic() - start_time)) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..6a6739b83 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/__pycache__/http.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/__pycache__/http.cpython-38.pyc new file mode 100644 index 000000000..c07013455 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/__pycache__/http.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/__pycache__/utils.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/__pycache__/utils.cpython-38.pyc new file mode 100644 index 000000000..176cb1193 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/__pycache__/utils.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/http.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/http.py new file mode 100644 index 000000000..aa1f88416 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/http.py @@ -0,0 +1,147 @@ +from typing import Dict +from typing import FrozenSet +from typing import Optional + +from ..context import Context +from ..internal.logger import get_logger +from .utils import get_wsgi_header + + +log = get_logger(__name__) + +# HTTP headers one should set for distributed tracing. +# These are cross-language (eg: Python, Go and other implementations should honor these) +HTTP_HEADER_TRACE_ID = "x-datadog-trace-id" +HTTP_HEADER_PARENT_ID = "x-datadog-parent-id" +HTTP_HEADER_SAMPLING_PRIORITY = "x-datadog-sampling-priority" +HTTP_HEADER_ORIGIN = "x-datadog-origin" + + +# Note that due to WSGI spec we have to also check for uppercased and prefixed +# versions of these headers +POSSIBLE_HTTP_HEADER_TRACE_IDS = frozenset([HTTP_HEADER_TRACE_ID, get_wsgi_header(HTTP_HEADER_TRACE_ID).lower()]) +POSSIBLE_HTTP_HEADER_PARENT_IDS = frozenset([HTTP_HEADER_PARENT_ID, get_wsgi_header(HTTP_HEADER_PARENT_ID).lower()]) +POSSIBLE_HTTP_HEADER_SAMPLING_PRIORITIES = frozenset( + [HTTP_HEADER_SAMPLING_PRIORITY, get_wsgi_header(HTTP_HEADER_SAMPLING_PRIORITY).lower()] +) +POSSIBLE_HTTP_HEADER_ORIGIN = frozenset([HTTP_HEADER_ORIGIN, get_wsgi_header(HTTP_HEADER_ORIGIN).lower()]) + + +class HTTPPropagator(object): + """A HTTP Propagator using HTTP headers as carrier.""" + + @staticmethod + def inject(span_context, headers): + # type: (Context, Dict[str, str]) -> None + """Inject Context attributes that have to be propagated as HTTP headers. + + Here is an example using `requests`:: + + import requests + from ddtrace.propagation.http import HTTPPropagator + + def parent_call(): + with tracer.trace('parent_span') as span: + headers = {} + HTTPPropagator.inject(span.context, headers) + url = '' + r = requests.get(url, headers=headers) + + :param Context span_context: Span context to propagate. + :param dict headers: HTTP headers to extend with tracing attributes. + """ + headers[HTTP_HEADER_TRACE_ID] = str(span_context.trace_id) + headers[HTTP_HEADER_PARENT_ID] = str(span_context.span_id) + sampling_priority = span_context.sampling_priority + # Propagate priority only if defined + if sampling_priority is not None: + headers[HTTP_HEADER_SAMPLING_PRIORITY] = str(span_context.sampling_priority) + # Propagate origin only if defined + if span_context.dd_origin is not None: + headers[HTTP_HEADER_ORIGIN] = str(span_context.dd_origin) + + @staticmethod + def _extract_header_value(possible_header_names, headers, default=None): + # type: (FrozenSet[str], Dict[str, str], Optional[str]) -> Optional[str] + for header in possible_header_names: + try: + return headers[header] + except KeyError: + pass + + return default + + @staticmethod + def extract(headers): + # type: (Dict[str,str]) -> Context + """Extract a Context from HTTP headers into a new Context. + + Here is an example from a web endpoint:: + + from ddtrace.propagation.http import HTTPPropagator + + def my_controller(url, headers): + context = HTTPPropagator.extract(headers) + if context: + tracer.context_provider.activate(context) + + with tracer.trace('my_controller') as span: + span.set_meta('http.url', url) + + :param dict headers: HTTP headers to extract tracing attributes. + :return: New `Context` with propagated attributes. + """ + if not headers: + return Context() + + try: + normalized_headers = {name.lower(): v for name, v in headers.items()} + # TODO: Fix variable type changing (mypy) + trace_id = HTTPPropagator._extract_header_value( + POSSIBLE_HTTP_HEADER_TRACE_IDS, + normalized_headers, + ) + if trace_id is None: + return Context() + + parent_span_id = HTTPPropagator._extract_header_value( + POSSIBLE_HTTP_HEADER_PARENT_IDS, + normalized_headers, + default="0", + ) + sampling_priority = HTTPPropagator._extract_header_value( + POSSIBLE_HTTP_HEADER_SAMPLING_PRIORITIES, + normalized_headers, + ) + origin = HTTPPropagator._extract_header_value( + POSSIBLE_HTTP_HEADER_ORIGIN, + normalized_headers, + ) + + # Try to parse values into their expected types + try: + if sampling_priority is not None: + sampling_priority = int(sampling_priority) # type: ignore[assignment] + else: + sampling_priority = sampling_priority + + return Context( + # DEV: Do not allow `0` for trace id or span id, use None instead + trace_id=int(trace_id) or None, + span_id=int(parent_span_id) or None, # type: ignore[arg-type] + sampling_priority=sampling_priority, # type: ignore[arg-type] + dd_origin=origin, + ) + # If headers are invalid and cannot be parsed, return a new context and log the issue. + except (TypeError, ValueError): + log.debug( + "received invalid x-datadog-* headers, trace-id: %r, parent-id: %r, priority: %r, origin: %r", + trace_id, + parent_span_id, + sampling_priority, + origin, + ) + return Context() + except Exception: + log.debug("error while extracting x-datadog-* headers", exc_info=True) + return Context() diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/utils.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/utils.py new file mode 100644 index 000000000..66a4b28f7 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/propagation/utils.py @@ -0,0 +1,31 @@ +from typing import Optional + +from ddtrace.utils.cache import cached + + +@cached() +def get_wsgi_header(header): + # type: (str) -> str + """Returns a WSGI compliant HTTP header. + See https://www.python.org/dev/peps/pep-3333/#environ-variables for + information from the spec. + """ + return "HTTP_{}".format(header.upper().replace("-", "_")) + + +@cached() +def from_wsgi_header(header): + # type: (str) -> Optional[str] + """Convert a WSGI compliant HTTP header into the original header. + See https://www.python.org/dev/peps/pep-3333/#environ-variables for + information from the spec. + """ + HTTP_PREFIX = "HTTP_" + # PEP 333 gives two headers which aren't prepended with HTTP_. + UNPREFIXED_HEADERS = {"CONTENT_TYPE", "CONTENT_LENGTH"} + + if header.startswith(HTTP_PREFIX): + header = header[len(HTTP_PREFIX) :] + elif header not in UNPREFIXED_HEADERS: + return None + return header.replace("_", "-").title() diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/provider.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/provider.py new file mode 100644 index 000000000..7d961c8e7 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/provider.py @@ -0,0 +1,138 @@ +import abc +from typing import Any +from typing import Callable +from typing import Optional +from typing import Union + +import six + +from . import _hooks +from .context import Context +from .internal.compat import contextvars +from .internal.logger import get_logger +from .span import Span + + +log = get_logger(__name__) + + +_DD_CONTEXTVAR = contextvars.ContextVar( + "datadog_contextvar", default=None +) # type: contextvars.ContextVar[Optional[Union[Context, Span]]] + + +class BaseContextProvider(six.with_metaclass(abc.ABCMeta)): + """ + A ``ContextProvider`` is an interface that provides the blueprint + for a callable class, capable to retrieve the current active + ``Context`` instance. Context providers must inherit this class + and implement: + * the ``active`` method, that returns the current active ``Context`` + * the ``activate`` method, that sets the current active ``Context`` + """ + + def __init__(self): + # type: (...) -> None + self._hooks = _hooks.Hooks() + + @abc.abstractmethod + def _has_active_context(self): + pass + + @abc.abstractmethod + def activate(self, ctx): + # type: (Optional[Union[Context, Span]]) -> None + self._hooks.emit(self.activate, ctx) + + @abc.abstractmethod + def active(self): + # type: () -> Optional[Union[Context, Span]] + pass + + def _on_activate(self, func): + # type: (Callable[[Optional[Union[Span, Context]]], Any]) -> Callable[[Optional[Union[Span, Context]]], Any] + """Register a function to execute when a span is activated. + + Can be used as a decorator. + + :param func: The function to call when a span is activated. + The activated span will be passed as argument. + """ + self._hooks.register(self.activate, func) + return func + + def _deregister_on_activate(self, func): + # type: (Callable[[Optional[Union[Span, Context]]], Any]) -> Callable[[Optional[Union[Span, Context]]], Any] + """Unregister a function registered to execute when a span is activated. + + Can be used as a decorator. + + :param func: The function to stop calling when a span is activated. + """ + + self._hooks.deregister(self.activate, func) + return func + + def __call__(self, *args, **kwargs): + """Method available for backward-compatibility. It proxies the call to + ``self.active()`` and must not do anything more. + """ + return self.active() + + +class DatadogContextMixin(object): + """Mixin that provides active span updating suitable for synchronous + and asynchronous executions. + """ + + def activate(self, ctx): + # type: (Optional[Union[Context, Span]]) -> None + raise NotImplementedError + + def _update_active(self, span): + # type: (Span) -> Optional[Span] + """Updates the active span in an executor. + + The active span is updated to be the span's parent if the span has + finished until an unfinished span is found. + """ + if span.finished: + new_active = span # type: Optional[Span] + while new_active and new_active.finished: + new_active = new_active._parent + self.activate(new_active) + return new_active + return span + + +class DefaultContextProvider(BaseContextProvider, DatadogContextMixin): + """Context provider that retrieves contexts from a context variable. + + It is suitable for synchronous programming and for asynchronous executors + that support contextvars. + """ + + def __init__(self): + # type: () -> None + super(DefaultContextProvider, self).__init__() + _DD_CONTEXTVAR.set(None) + + def _has_active_context(self): + # type: () -> bool + """Returns whether there is an active context in the current execution.""" + ctx = _DD_CONTEXTVAR.get() + return ctx is not None + + def activate(self, ctx): + # type: (Optional[Union[Span, Context]]) -> None + """Makes the given context active in the current execution.""" + _DD_CONTEXTVAR.set(ctx) + super(DefaultContextProvider, self).activate(ctx) + + def active(self): + # type: () -> Optional[Union[Context, Span]] + """Returns the active span or context for the current execution.""" + item = _DD_CONTEXTVAR.get() + if isinstance(item, Span): + return self._update_active(item) + return item diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/py.typed b/flask-app/venv/lib/python3.8/site-packages/ddtrace/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/runtime/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/runtime/__init__.py new file mode 100644 index 000000000..6bd7a5aae --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/runtime/__init__.py @@ -0,0 +1,60 @@ +from typing import Optional + +import six + +import ddtrace.internal.runtime.runtime_metrics + + +class _RuntimeMetricsStatus(type): + @property + def _enabled(_): + # type: () -> bool + """Runtime metrics enabled status.""" + return ddtrace.internal.runtime.runtime_metrics.RuntimeWorker.enabled + + +class RuntimeMetrics(six.with_metaclass(_RuntimeMetricsStatus)): + """ + Runtime metrics service API. + + This is normally started automatically by ``ddtrace-run`` when the + ``DD_RUNTIME_METRICS_ENABLED`` variable is set. + + To start the service manually, invoke the ``enable`` static method:: + + from ddtrace.runtime import RuntimeMetrics + RuntimeMetrics.enable() + """ + + @staticmethod + def enable(tracer=None, dogstatsd_url=None, flush_interval=None): + # type: (Optional[ddtrace.Tracer], Optional[str], Optional[float]) -> None + """ + Enable the runtime metrics collection service. + + If the service has already been activated before, this method does + nothing. Use ``disable`` to turn off the runtime metric collection + service. + + :param tracer: The tracer instance to correlate with. + :param dogstatsd_url: The DogStatsD URL. + :param flush_interval: The flush interval. + """ + + ddtrace.internal.runtime.runtime_metrics.RuntimeWorker.enable( + tracer=tracer, dogstatsd_url=dogstatsd_url, flush_interval=flush_interval + ) + + @staticmethod + def disable(): + # type: () -> None + """ + Disable the runtime metrics collection service. + + Once disabled, runtime metrics can be re-enabled by calling ``enable`` + again. + """ + ddtrace.internal.runtime.runtime_metrics.RuntimeWorker.disable() + + +__all__ = ["RuntimeMetrics"] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/runtime/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/runtime/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..c84c4b68c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/runtime/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/sampler.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/sampler.py new file mode 100644 index 000000000..7195c6302 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/sampler.py @@ -0,0 +1,443 @@ +"""Samplers manage the client-side trace sampling + +Any `sampled = False` trace won't be written, and can be ignored by the instrumentation. +""" +import abc +import json +from typing import Any +from typing import Dict +from typing import List +from typing import Optional +from typing import TYPE_CHECKING + +import six + +from .constants import AUTO_KEEP +from .constants import AUTO_REJECT +from .constants import ENV_KEY +from .constants import SAMPLING_AGENT_DECISION +from .constants import SAMPLING_LIMIT_DECISION +from .constants import SAMPLING_RULE_DECISION +from .internal.compat import iteritems +from .internal.compat import pattern_type +from .internal.logger import get_logger +from .internal.rate_limiter import RateLimiter +from .utils.formats import get_env + + +try: + from json.decoder import JSONDecodeError +except ImportError: + # handling python 2.X import error + JSONDecodeError = ValueError # type: ignore + +if TYPE_CHECKING: + from .span import Span + + +log = get_logger(__name__) + +MAX_TRACE_ID = 2 ** 64 + +# Has to be the same factor and key as the Agent to allow chained sampling +KNUTH_FACTOR = 1111111111111111111 + + +class BaseSampler(six.with_metaclass(abc.ABCMeta)): + @abc.abstractmethod + def sample(self, span): + pass + + +class BasePrioritySampler(BaseSampler): + @abc.abstractmethod + def update_rate_by_service_sample_rates(self, sample_rates): + pass + + +class AllSampler(BaseSampler): + """Sampler sampling all the traces""" + + def sample(self, span): + # type: (Span) -> bool + return True + + +class RateSampler(BaseSampler): + """Sampler based on a rate + + Keep (100 * `sample_rate`)% of the traces. + It samples randomly, its main purpose is to reduce the instrumentation footprint. + """ + + def __init__(self, sample_rate=1.0): + # type: (float) -> None + if sample_rate < 0.0: + raise ValueError("sample_rate of {} is negative".format(sample_rate)) + elif sample_rate > 1.0: + sample_rate = 1.0 + + self.set_sample_rate(sample_rate) + + log.debug("initialized RateSampler, sample %s%% of traces", 100 * sample_rate) + + def set_sample_rate(self, sample_rate): + # type: (float) -> None + self.sample_rate = float(sample_rate) + self.sampling_id_threshold = self.sample_rate * MAX_TRACE_ID + + def sample(self, span): + # type: (Span) -> bool + return ((span.trace_id * KNUTH_FACTOR) % MAX_TRACE_ID) <= self.sampling_id_threshold + + +class RateByServiceSampler(BasePrioritySampler): + """Sampler based on a rate, by service + + Keep (100 * `sample_rate`)% of the traces. + The sample rate is kept independently for each service/env tuple. + """ + + _default_key = "service:,env:" + + @staticmethod + def _key( + service=None, # type: Optional[str] + env=None, # type: Optional[str] + ): + # type: (...) -> str + """Compute a key with the same format used by the Datadog agent API.""" + service = service or "" + env = env or "" + return "service:" + service + ",env:" + env + + def __init__(self, sample_rate=1.0): + # type: (float) -> None + self.sample_rate = sample_rate + self._by_service_samplers = self._get_new_by_service_sampler() + + def _get_new_by_service_sampler(self): + # type: () -> Dict[str, RateSampler] + return {self._default_key: RateSampler(self.sample_rate)} + + def set_sample_rate( + self, + sample_rate, # type: float + service="", # type: str + env="", # type: str + ): + # type: (...) -> None + self._by_service_samplers[self._key(service, env)] = RateSampler(sample_rate) + + def sample(self, span): + # type: (Span) -> bool + tags = span.tracer.tags if span.tracer else {} + env = tags[ENV_KEY] if ENV_KEY in tags else None + key = self._key(span.service, env) + + sampler = self._by_service_samplers.get(key, self._by_service_samplers[self._default_key]) + span.set_metric(SAMPLING_AGENT_DECISION, sampler.sample_rate) + return sampler.sample(span) + + def update_rate_by_service_sample_rates(self, rate_by_service): + # type: (Dict[str, float]) -> None + new_by_service_samplers = self._get_new_by_service_sampler() + for key, sample_rate in iteritems(rate_by_service): + new_by_service_samplers[key] = RateSampler(sample_rate) + + self._by_service_samplers = new_by_service_samplers + + +class DatadogSampler(BasePrioritySampler): + __slots__ = ("default_sampler", "limiter", "rules") + + NO_RATE_LIMIT = -1 + DEFAULT_RATE_LIMIT = 100 + + def __init__( + self, + rules=None, # type: Optional[List[SamplingRule]] + default_sample_rate=None, # type: Optional[float] + rate_limit=None, # type: Optional[int] + ): + # type: (...) -> None + """ + Constructor for DatadogSampler sampler + + :param rules: List of :class:`SamplingRule` rules to apply to the root span of every trace, default no rules + :type rules: :obj:`list` of :class:`SamplingRule` + :param default_sample_rate: The default sample rate to apply if no rules matched (default: ``None`` / + Use :class:`RateByServiceSampler` only) + :type default_sample_rate: float 0 <= X <= 1.0 + :param rate_limit: Global rate limit (traces per second) to apply to all traces regardless of the rules + applied to them, (default: ``100``) + :type rate_limit: :obj:`int` + """ + if default_sample_rate is None: + sample_rate = get_env("trace", "sample_rate") + if sample_rate is not None: + default_sample_rate = float(sample_rate) + + if rate_limit is None: + rate_limit = int(get_env("trace", "rate_limit", default=self.DEFAULT_RATE_LIMIT)) # type: ignore[arg-type] + + # Ensure rules is a list + if rules is None: + env_sampling_rules = get_env("trace", "sampling_rules") + if env_sampling_rules: + rules = self._parse_rules_from_env_variable(env_sampling_rules) + else: + rules = [] + + # Validate that the rules is a list of SampleRules + for rule in rules: + if not isinstance(rule, SamplingRule): + raise TypeError("Rule {!r} must be a sub-class of type ddtrace.sampler.SamplingRules".format(rule)) + self.rules = rules + + # Configure rate limiter + self.limiter = RateLimiter(rate_limit) + + if default_sample_rate is None: + log.debug("initialized DatadogSampler, limit %r traces per second", rate_limit) + # Default to previous default behavior of RateByServiceSampler + self.default_sampler = RateByServiceSampler() # type: BaseSampler + else: + log.debug( + "initialized DatadogSampler, sample %s%% traces, limit %r traces per second", + 100 * default_sample_rate, + rate_limit, + ) + self.default_sampler = SamplingRule(sample_rate=default_sample_rate) + + def _parse_rules_from_env_variable(self, rules): + sampling_rules = [] + if rules is not None: + json_rules = [] + try: + json_rules = json.loads(rules) + except JSONDecodeError: + raise ValueError("Unable to parse DD_TRACE_SAMPLING_RULES={}".format(rules)) + for rule in json_rules: + if "sample_rate" not in rule: + raise KeyError("No sample_rate provided for sampling rule: {}".format(json.dumps(rule))) + sample_rate = float(rule["sample_rate"]) + service = rule.get("service", SamplingRule.NO_RULE) + name = rule.get("name", SamplingRule.NO_RULE) + try: + sampling_rule = SamplingRule(sample_rate=sample_rate, service=service, name=name) + except ValueError as e: + raise ValueError("Error creating sampling rule {}: {}".format(json.dumps(rule), e)) + sampling_rules.append(sampling_rule) + return sampling_rules + + def update_rate_by_service_sample_rates(self, sample_rates): + # type: (Dict[str, float]) -> None + # Pass through the call to our RateByServiceSampler + if isinstance(self.default_sampler, RateByServiceSampler): + self.default_sampler.update_rate_by_service_sample_rates(sample_rates) + + def _set_priority(self, span, priority): + # type: (Span, int) -> None + span.context.sampling_priority = priority + span.sampled = priority is AUTO_KEEP + + def sample(self, span): + # type: (Span) -> bool + """ + Decide whether the provided span should be sampled or not + + The span provided should be the root span in the trace. + + :param span: The root span of a trace + :type span: :class:`ddtrace.span.Span` + :returns: Whether the span was sampled or not + :rtype: :obj:`bool` + """ + # If there are rules defined, then iterate through them and find one that wants to sample + matching_rule = None # type: Optional[BaseSampler] + # Go through all rules and grab the first one that matched + # DEV: This means rules should be ordered by the user from most specific to least specific + for rule in self.rules: + if rule.matches(span): + matching_rule = rule + break + else: + # If this is the old sampler, sample and return + if isinstance(self.default_sampler, RateByServiceSampler): + if self.default_sampler.sample(span): + self._set_priority(span, AUTO_KEEP) + return True + else: + self._set_priority(span, AUTO_REJECT) + return False + + # If no rules match, use our default sampler + matching_rule = self.default_sampler + + # Sample with the matching sampling rule + if isinstance(matching_rule, (RateSampler, SamplingRule)): + span.set_metric(SAMPLING_RULE_DECISION, matching_rule.sample_rate) + if not matching_rule.sample(span): + self._set_priority(span, AUTO_REJECT) + return False + else: + # Do not return here, we need to apply rate limit + self._set_priority(span, AUTO_KEEP) + + # Ensure all allowed traces adhere to the global rate limit + allowed = self.limiter.is_allowed() + # Always set the sample rate metric whether it was allowed or not + # DEV: Setting this allows us to properly compute metrics and debug the + # various sample rates that are getting applied to this span + span.set_metric(SAMPLING_LIMIT_DECISION, self.limiter.effective_rate) + if not allowed: + self._set_priority(span, AUTO_REJECT) + return False + + # We made it by all of checks, sample this trace + self._set_priority(span, AUTO_KEEP) + return True + + +class SamplingRule(BaseSampler): + """ + Definition of a sampling rule used by :class:`DatadogSampler` for applying a sample rate on a span + """ + + __slots__ = ("_sample_rate", "_sampling_id_threshold", "service", "name") + + NO_RULE = object() + + def __init__( + self, + sample_rate, # type: float + service=NO_RULE, # type: Any + name=NO_RULE, # type: Any + ): + # type: (...) -> None + """ + Configure a new :class:`SamplingRule` + + .. code:: python + + DatadogSampler([ + # Sample 100% of any trace + SamplingRule(sample_rate=1.0), + + # Sample no healthcheck traces + SamplingRule(sample_rate=0, name='flask.request'), + + # Sample all services ending in `-db` based on a regular expression + SamplingRule(sample_rate=0.5, service=re.compile('-db$')), + + # Sample based on service name using custom function + SamplingRule(sample_rate=0.75, service=lambda service: 'my-app' in service), + ]) + + :param sample_rate: The sample rate to apply to any matching spans + :type sample_rate: :obj:`float` greater than or equal to 0.0 and less than or equal to 1.0 + :param service: Rule to match the `span.service` on, default no rule defined + :type service: :obj:`object` to directly compare, :obj:`function` to evaluate, or :class:`re.Pattern` to match + :param name: Rule to match the `span.name` on, default no rule defined + :type name: :obj:`object` to directly compare, :obj:`function` to evaluate, or :class:`re.Pattern` to match + """ + # Enforce sample rate constraints + if not 0.0 <= sample_rate <= 1.0: + raise ValueError( + ( + "SamplingRule(sample_rate={}) must be greater than or equal to 0.0 and less than or equal to 1.0" + ).format(sample_rate) + ) + + self.sample_rate = sample_rate + self.service = service + self.name = name + + @property + def sample_rate(self): + # type: () -> float + return self._sample_rate + + @sample_rate.setter + def sample_rate(self, sample_rate): + # type: (float) -> None + self._sample_rate = sample_rate + self._sampling_id_threshold = sample_rate * MAX_TRACE_ID + + def _pattern_matches(self, prop, pattern): + # If the rule is not set, then assume it matches + # DEV: Having no rule and being `None` are different things + # e.g. ignoring `span.service` vs `span.service == None` + if pattern is self.NO_RULE: + return True + + # If the pattern is callable (e.g. a function) then call it passing the prop + # The expected return value is a boolean so cast the response in case it isn't + if callable(pattern): + try: + return bool(pattern(prop)) + except Exception: + log.warning("%r pattern %r failed with %r", self, pattern, prop, exc_info=True) + # Their function failed to validate, assume it is a False + return False + + # The pattern is a regular expression and the prop is a string + if isinstance(pattern, pattern_type): + try: + return bool(pattern.match(str(prop))) + except (ValueError, TypeError): + # This is to guard us against the casting to a string (shouldn't happen, but still) + log.warning("%r pattern %r failed with %r", self, pattern, prop, exc_info=True) + return False + + # Exact match on the values + return prop == pattern + + def matches(self, span): + # type: (Span) -> bool + """ + Return if this span matches this rule + + :param span: The span to match against + :type span: :class:`ddtrace.span.Span` + :returns: Whether this span matches or not + :rtype: :obj:`bool` + """ + return all( + self._pattern_matches(prop, pattern) + for prop, pattern in [ + (span.service, self.service), + (span.name, self.name), + ] + ) + + def sample(self, span): + # type: (Span) -> bool + """ + Return if this rule chooses to sample the span + + :param span: The span to sample against + :type span: :class:`ddtrace.span.Span` + :returns: Whether this span was sampled + :rtype: :obj:`bool` + """ + if self.sample_rate == 1: + return True + elif self.sample_rate == 0: + return False + + return ((span.trace_id * KNUTH_FACTOR) % MAX_TRACE_ID) <= self._sampling_id_threshold + + def _no_rule_or_self(self, val): + return "NO_RULE" if val is self.NO_RULE else val + + def __repr__(self): + return "{}(sample_rate={!r}, service={!r}, name={!r})".format( + self.__class__.__name__, + self.sample_rate, + self._no_rule_or_self(self.service), + self._no_rule_or_self(self.name), + ) + + __str__ = __repr__ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__init__.py new file mode 100644 index 000000000..2c3a0bf78 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__init__.py @@ -0,0 +1,17 @@ +from .._hooks import Hooks +from .config import Config +from .exceptions import ConfigException +from .http import HttpConfig +from .integration import IntegrationConfig + + +# Default global config +_config = Config() + +__all__ = [ + "Config", + "ConfigException", + "HttpConfig", + "Hooks", + "IntegrationConfig", +] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..50cb65232 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__pycache__/config.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__pycache__/config.cpython-38.pyc new file mode 100644 index 000000000..a2eb777c3 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__pycache__/config.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__pycache__/exceptions.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__pycache__/exceptions.cpython-38.pyc new file mode 100644 index 000000000..c7be889b3 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__pycache__/exceptions.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__pycache__/http.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__pycache__/http.cpython-38.pyc new file mode 100644 index 000000000..aefa61d3c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__pycache__/http.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__pycache__/integration.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__pycache__/integration.cpython-38.pyc new file mode 100644 index 000000000..1546d9bfd Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/__pycache__/integration.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/config.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/config.py new file mode 100644 index 000000000..cc57b5bee --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/config.py @@ -0,0 +1,244 @@ +from copy import deepcopy +import os +from typing import List +from typing import Optional +from typing import Tuple + +from ddtrace.utils.cache import cachedmethod + +from ..internal.logger import get_logger +from ..pin import Pin +from ..utils.deprecation import get_service_legacy +from ..utils.formats import asbool +from ..utils.formats import get_env +from ..utils.formats import parse_tags_str +from .http import HttpConfig +from .integration import IntegrationConfig + + +log = get_logger(__name__) + + +# Borrowed from: https://stackoverflow.com/questions/20656135/python-deep-merge-dictionary-data#20666342 +def _deepmerge(source, destination): + """ + Merge the first provided ``dict`` into the second. + + :param dict source: The ``dict`` to merge into ``destination`` + :param dict destination: The ``dict`` that should get updated + :rtype: dict + :returns: ``destination`` modified + """ + for key, value in source.items(): + if isinstance(value, dict): + # get node or create one + node = destination.setdefault(key, {}) + _deepmerge(value, node) + else: + destination[key] = value + + return destination + + +def get_error_ranges(error_range_str): + # type: (str) -> List[Tuple[int, int]] + error_ranges = [] + error_range_str = error_range_str.strip() + error_ranges_str = error_range_str.split(",") + for error_range in error_ranges_str: + values = error_range.split("-") + try: + # Note: mypy does not like variable type changing + values = [int(v) for v in values] # type: ignore[misc] + except ValueError: + log.exception("Error status codes was not a number %s", values) + continue + error_range = (min(values), max(values)) # type: ignore[assignment] + error_ranges.append(error_range) + return error_ranges # type: ignore[return-value] + + +class Config(object): + """Configuration object that exposes an API to set and retrieve + global settings for each integration. All integrations must use + this instance to register their defaults, so that they're public + available and can be updated by users. + """ + + class HTTPServerConfig(object): + _error_statuses = "500-599" # type: str + _error_ranges = get_error_ranges(_error_statuses) # type: List[Tuple[int, int]] + + @property + def error_statuses(self): + # type: () -> str + return self._error_statuses + + @error_statuses.setter + def error_statuses(self, value): + # type: (str) -> None + self._error_statuses = value + self._error_ranges = get_error_ranges(value) + # Mypy can't catch cached method's invalidate() + self.is_error_code.invalidate() # type: ignore[attr-defined] + + @property + def error_ranges(self): + # type: () -> List[Tuple[int, int]] + return self._error_ranges + + @cachedmethod() + def is_error_code(self, status_code): + # type: (int) -> bool + """Returns a boolean representing whether or not a status code is an error code. + Error status codes by default are 500-599. + You may also enable custom error codes:: + + from ddtrace import config + config.http_server.error_statuses = '401-404,419' + + Ranges and singular error codes are permitted and can be separated using commas. + """ + for error_range in self.error_ranges: + if error_range[0] <= status_code <= error_range[1]: + return True + return False + + def __init__(self): + # use a dict as underlying storing mechanism + self._config = {} + + header_tags = parse_tags_str(get_env("trace", "header_tags") or "") + self.http = HttpConfig(header_tags=header_tags) + + # Master switch for turning on and off trace search by default + # this weird invocation of get_env is meant to read the DD_ANALYTICS_ENABLED + # legacy environment variable. It should be removed in the future + legacy_config_value = get_env("analytics", "enabled", default=False) + + self.analytics_enabled = asbool(get_env("trace", "analytics_enabled", default=legacy_config_value)) + + self.tags = parse_tags_str(os.getenv("DD_TAGS") or "") + + self.env = os.getenv("DD_ENV") or self.tags.get("env") + # DEV: we don't use `self._get_service()` here because {DD,DATADOG}_SERVICE and + # {DD,DATADOG}_SERVICE_NAME (deprecated) are distinct functionalities. + self.service = os.getenv("DD_SERVICE") or os.getenv("DATADOG_SERVICE") or self.tags.get("service") + self.version = os.getenv("DD_VERSION") or self.tags.get("version") + self.http_server = self.HTTPServerConfig() + + self.service_mapping = parse_tags_str(get_env("service", "mapping", default="")) + + # The service tag corresponds to span.service and should not be + # included in the global tags. + if self.service and "service" in self.tags: + del self.tags["service"] + + # The version tag should not be included on all spans. + if self.version and "version" in self.tags: + del self.tags["version"] + + self.logs_injection = asbool(get_env("logs", "injection", default=False)) + + self.report_hostname = asbool(get_env("trace", "report_hostname", default=False)) + + self.health_metrics_enabled = asbool(get_env("trace", "health_metrics_enabled", default=False)) + + # Raise certain errors only if in testing raise mode to prevent crashing in production with non-critical errors + self._raise = asbool(os.getenv("DD_TESTING_RAISE", False)) + + def __getattr__(self, name): + if name not in self._config: + self._config[name] = IntegrationConfig(self, name) + + return self._config[name] + + def get_from(self, obj): + """Retrieves the configuration for the given object. + Any object that has an attached `Pin` must have a configuration + and if a wrong object is given, an empty `dict` is returned + for safety reasons. + """ + pin = Pin.get_from(obj) + if pin is None: + log.debug("No configuration found for %s", obj) + return {} + + return pin._config + + def _add(self, integration, settings, merge=True): + """Internal API that registers an integration with given default + settings. + + :param str integration: The integration name (i.e. `requests`) + :param dict settings: A dictionary that contains integration settings; + to preserve immutability of these values, the dictionary is copied + since it contains integration defaults. + :param bool merge: Whether to merge any existing settings with those provided, + or if we should overwrite the settings with those provided; + Note: when merging existing settings take precedence. + """ + # DEV: Use `getattr()` to call our `__getattr__` helper + existing = getattr(self, integration) + settings = deepcopy(settings) + + if merge: + # DEV: This may appear backwards keeping `existing` as the "source" and `settings` as + # the "destination", but we do not want to let `_add(..., merge=True)` overwrite any + # existing settings + # + # >>> config.requests['split_by_domain'] = True + # >>> config._add('requests', dict(split_by_domain=False)) + # >>> config.requests['split_by_domain'] + # True + self._config[integration] = IntegrationConfig(self, integration, _deepmerge(existing, settings)) + else: + self._config[integration] = IntegrationConfig(self, integration, settings) + + def trace_headers(self, whitelist): + """ + Registers a set of headers to be traced at global level or integration level. + :param whitelist: the case-insensitive list of traced headers + :type whitelist: list of str or str + :return: self + :rtype: HttpConfig + """ + self.http.trace_headers(whitelist) + return self + + def header_is_traced(self, header_name): + # type: (str) -> bool + """ + Returns whether or not the current header should be traced. + :param header_name: the header name + :type header_name: str + :rtype: bool + """ + return self.http.header_is_traced(header_name) + + def _header_tag_name(self, header_name): + # type: (str) -> Optional[str] + return self.http._header_tag_name(header_name) + + def _get_service(self, default=None): + """ + Returns the globally configured service. + + If a service is not configured globally, attempts to get the service + using the legacy environment variables via ``get_service_legacy`` + else ``default`` is returned. + + When support for {DD,DATADOG}_SERVICE_NAME is removed, all usages of + this method can be replaced with `config.service`. + + :param default: the default service to use if none is configured or + found. + :type default: str + :rtype: str|None + """ + return self.service if self.service is not None else get_service_legacy(default=default) + + def __repr__(self): + cls = self.__class__ + integrations = ", ".join(self._config.keys()) + return "{}.{}({})".format(cls.__module__, cls.__name__, integrations) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/exceptions.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/exceptions.py new file mode 100644 index 000000000..c11b83be3 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/exceptions.py @@ -0,0 +1,6 @@ +class ConfigException(Exception): + """Configuration exception when an integration that is not available + is called in the `Config` object. + """ + + pass diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/http.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/http.py new file mode 100644 index 000000000..72af47dd8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/http.py @@ -0,0 +1,83 @@ +from typing import List +from typing import Mapping +from typing import Optional +from typing import Union + +import six + +from ..internal.logger import get_logger +from ..utils.cache import cachedmethod +from ..utils.http import normalize_header_name + + +log = get_logger(__name__) + + +class HttpConfig(object): + """ + Configuration object that expose an API to set and retrieve both global and integration specific settings + related to the http context. + """ + + def __init__(self, header_tags=None): + # type: (Optional[Mapping[str, str]]) -> None + self._header_tags = {normalize_header_name(k): v for k, v in header_tags.items()} if header_tags else {} + self.trace_query_string = None + + @cachedmethod() + def _header_tag_name(self, header_name): + # type: (str) -> Optional[str] + if not self._header_tags: + return None + + normalized_header_name = normalize_header_name(header_name) + log.debug( + "Checking header '%s' tracing in whitelist %s", normalized_header_name, six.viewkeys(self._header_tags) + ) + return self._header_tags.get(normalized_header_name) + + @property + def is_header_tracing_configured(self): + # type: () -> bool + return len(self._header_tags) > 0 + + def trace_headers(self, whitelist): + # type: (Union[List[str], str]) -> Optional[HttpConfig] + """ + Registers a set of headers to be traced at global level or integration level. + :param whitelist: the case-insensitive list of traced headers + :type whitelist: list of str or str + :return: self + :rtype: HttpConfig + """ + if not whitelist: + return None + + whitelist = [whitelist] if isinstance(whitelist, str) else whitelist + for whitelist_entry in whitelist: + normalized_header_name = normalize_header_name(whitelist_entry) + if not normalized_header_name: + continue + # Empty tag is replaced by the default tag for this header: + # Host on the request defaults to http.request.headers.host + self._header_tags.setdefault(normalized_header_name, "") + + # Mypy can't catch cached method's invalidate() + self._header_tag_name.invalidate() # type: ignore[attr-defined] + + return self + + def header_is_traced(self, header_name): + # type: (str) -> bool + """ + Returns whether or not the current header should be traced. + :param header_name: the header name + :type header_name: str + :rtype: bool + """ + return self._header_tag_name(header_name) is not None + + def __repr__(self): + return "<{} traced_headers={} trace_query_string={}>".format( + self.__class__.__name__, self._header_tags.keys(), self.trace_query_string + ) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/integration.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/integration.py new file mode 100644 index 000000000..401f32bb4 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/settings/integration.py @@ -0,0 +1,135 @@ +import os +from typing import Optional +from typing import Tuple + +from .._hooks import Hooks +from ..utils.attrdict import AttrDict +from ..utils.formats import asbool +from ..utils.formats import get_env +from .http import HttpConfig + + +class IntegrationConfig(AttrDict): + """ + Integration specific configuration object. + + This is what you will get when you do:: + + from ddtrace import config + + # This is an `IntegrationConfig` + config.flask + + # `IntegrationConfig` supports both attribute and item accessors + config.flask['service_name'] = 'my-service-name' + config.flask.service_name = 'my-service-name' + """ + + def __init__(self, global_config, name, *args, **kwargs): + """ + :param global_config: + :type global_config: Config + :param args: + :param kwargs: + """ + super(IntegrationConfig, self).__init__(*args, **kwargs) + + # Set internal properties for this `IntegrationConfig` + # DEV: By-pass the `__setattr__` overrides from `AttrDict` to set real properties + object.__setattr__(self, "global_config", global_config) + object.__setattr__(self, "integration_name", name) + object.__setattr__(self, "hooks", Hooks()) + object.__setattr__(self, "http", HttpConfig()) + + analytics_enabled, analytics_sample_rate = self._get_analytics_settings() + self.setdefault("analytics_enabled", analytics_enabled) + self.setdefault("analytics_sample_rate", float(analytics_sample_rate)) + + service = get_env(name, "service", default=get_env(name, "service_name", default=None)) + self.setdefault("service", service) + # TODO[v1.0]: this is required for backwards compatibility since some + # integrations use service_name instead of service. These should be + # unified. + self.setdefault("service_name", service) + + def _get_analytics_settings(self): + # type: () -> Tuple[Optional[bool], float] + # Set default analytics configuration, default is disabled + # DEV: Default to `None` which means do not set this key + # Inject environment variables for integration + _ = os.environ.get("DD_TRACE_%s_ANALYTICS_ENABLED" % self.integration_name.upper()) or get_env( + self.integration_name, "analytics_enabled" + ) + analytics_enabled = asbool(_) if _ is not None else None + + analytics_sample_rate = float( + os.environ.get("DD_TRACE_%s_ANALYTICS_SAMPLE_RATE" % self.integration_name.upper()) + or get_env(self.integration_name, "analytics_sample_rate") + or 1.0 + ) + + return analytics_enabled, analytics_sample_rate + + @property + def trace_query_string(self): + if self.http.trace_query_string is not None: + return self.http.trace_query_string + return self.global_config.http.trace_query_string + + @property + def is_header_tracing_configured(self): + # type: (...) -> bool + """Returns whether header tracing is enabled for this integration. + + Will return true if traced headers are configured for this integration + or if they are configured globally. + """ + return self.http.is_header_tracing_configured or self.global_config.http.is_header_tracing_configured + + def header_is_traced(self, header_name): + # type: (str) -> bool + """ + Returns whether or not the current header should be traced. + :param header_name: the header name + :type header_name: str + :rtype: bool + """ + return self._header_tag_name(header_name) is not None + + def _header_tag_name(self, header_name): + # type: (str) -> Optional[str] + tag_name = self.http._header_tag_name(header_name) + if tag_name is None: + return self.global_config._header_tag_name(header_name) + return tag_name + + def _is_analytics_enabled(self, use_global_config): + # DEV: analytics flag can be None which should not be taken as + # enabled when global flag is disabled + if use_global_config and self.global_config.analytics_enabled: + return self.analytics_enabled is not False + else: + return self.analytics_enabled is True + + def get_analytics_sample_rate(self, use_global_config=False): + """ + Returns analytics sample rate but only when integration-specific + analytics configuration is enabled with optional override with global + configuration + """ + if self._is_analytics_enabled(use_global_config): + analytics_sample_rate = getattr(self, "analytics_sample_rate", None) + # return True if attribute is None or attribute not found + if analytics_sample_rate is None: + return True + # otherwise return rate + return analytics_sample_rate + + # Use `None` as a way to say that it was not defined, + # `False` would mean `0` which is a different thing + return None + + def __repr__(self): + cls = self.__class__ + keys = ", ".join(self.keys()) + return "{}.{}({})".format(cls.__module__, cls.__name__, keys) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/span.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/span.py new file mode 100644 index 000000000..8f7cfb0b3 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/span.py @@ -0,0 +1,538 @@ +import math +import pprint +import sys +import traceback +from typing import Any +from typing import Callable +from typing import Dict +from typing import List +from typing import Optional +from typing import TYPE_CHECKING +from typing import Text +from typing import Union + +import six + +from . import config +from .constants import ERROR_MSG +from .constants import ERROR_STACK +from .constants import ERROR_TYPE +from .constants import MANUAL_DROP_KEY +from .constants import MANUAL_KEEP_KEY +from .constants import NUMERIC_TAGS +from .constants import SERVICE_KEY +from .constants import SERVICE_VERSION_KEY +from .constants import SPAN_MEASURED_KEY +from .constants import USER_KEEP +from .constants import USER_REJECT +from .constants import VERSION_KEY +from .context import Context +from .ext import SpanTypes +from .ext import http +from .ext import net +from .internal import _rand +from .internal.compat import NumericType +from .internal.compat import StringIO +from .internal.compat import ensure_text +from .internal.compat import is_integer +from .internal.compat import iteritems +from .internal.compat import numeric_types +from .internal.compat import stringify +from .internal.compat import time_ns +from .internal.logger import get_logger + + +if TYPE_CHECKING: + from .tracer import Tracer + + +_TagNameType = Union[Text, bytes] +_MetaDictType = Dict[_TagNameType, Text] +_MetricDictType = Dict[_TagNameType, NumericType] + +log = get_logger(__name__) + + +class Span(object): + + __slots__ = [ + # Public span attributes + "service", + "name", + "_resource", + "span_id", + "trace_id", + "parent_id", + "meta", + "error", + "metrics", + "_span_type", + "start_ns", + "duration_ns", + "tracer", + # Sampler attributes + "sampled", + # Internal attributes + "_context", + "_local_root", + "_parent", + "_ignored_exceptions", + "_on_finish_callbacks", + "__weakref__", + ] + + def __init__( + self, + tracer, # type: Optional[Tracer] + name, # type: str + service=None, # type: Optional[str] + resource=None, # type: Optional[str] + span_type=None, # type: Optional[str] + trace_id=None, # type: Optional[int] + span_id=None, # type: Optional[int] + parent_id=None, # type: Optional[int] + start=None, # type: Optional[int] + context=None, # type: Optional[Context] + on_finish=None, # type: Optional[List[Callable[[Span], None]]] + ): + # type: (...) -> None + """ + Create a new span. Call `finish` once the traced operation is over. + + :param ddtrace.Tracer tracer: the tracer that will submit this span when + finished. + :param str name: the name of the traced operation. + + :param str service: the service name + :param str resource: the resource name + :param str span_type: the span type + + :param int trace_id: the id of this trace's root span. + :param int parent_id: the id of this span's direct parent span. + :param int span_id: the id of this span. + + :param int start: the start time of request as a unix epoch in seconds + :param object context: the Context of the span. + :param on_finish: list of functions called when the span finishes. + """ + # pre-conditions + if not (span_id is None or isinstance(span_id, six.integer_types)): + raise TypeError("span_id must be an integer") + if not (trace_id is None or isinstance(trace_id, six.integer_types)): + raise TypeError("trace_id must be an integer") + if not (parent_id is None or isinstance(parent_id, six.integer_types)): + raise TypeError("parent_id must be an integer") + + # required span info + self.name = name + self.service = service + self._resource = [resource or name] + self._span_type = None + self.span_type = span_type + + # tags / metadata + self.meta = {} # type: _MetaDictType + self.error = 0 + self.metrics = {} # type: _MetricDictType + + # timing + self.start_ns = time_ns() if start is None else int(start * 1e9) + self.duration_ns = None # type: Optional[int] + + # tracing + self.trace_id = trace_id or _rand.rand64bits() # type: int + self.span_id = span_id or _rand.rand64bits() # type: int + self.parent_id = parent_id # type: Optional[int] + self.tracer = tracer # type: Optional[Tracer] + self._on_finish_callbacks = [] if on_finish is None else on_finish + + # sampling + self.sampled = True # type: bool + + self._context = context._with_span(self) if context else None # type: Optional[Context] + self._parent = None # type: Optional[Span] + self._ignored_exceptions = None # type: Optional[List[Exception]] + self._local_root = None # type: Optional[Span] + + def _ignore_exception(self, exc): + # type: (Exception) -> None + if self._ignored_exceptions is None: + self._ignored_exceptions = [exc] + else: + self._ignored_exceptions.append(exc) + + @property + def start(self): + # type: () -> float + """The start timestamp in Unix epoch seconds.""" + return self.start_ns / 1e9 + + @start.setter + def start(self, value): + # type: (Union[int, float]) -> None + self.start_ns = int(value * 1e9) + + @property + def resource(self): + return self._resource[0] + + @resource.setter + def resource(self, value): + self._resource[0] = value + + @property + def span_type(self): + return self._span_type + + @span_type.setter + def span_type(self, value): + self._span_type = value.value if isinstance(value, SpanTypes) else value + + @property + def finished(self): + # type: () -> bool + return self.duration_ns is not None + + @finished.setter + def finished(self, value): + # type: (bool) -> None + """Finishes the span if set to a truthy value. + + If the span is already finished and a truthy value is provided + no action will occur. + """ + if value: + if not self.finished: + self.duration_ns = time_ns() - self.start_ns + else: + self.duration_ns = None + + @property + def duration(self): + # type: () -> Optional[float] + """The span duration in seconds.""" + if self.duration_ns is not None: + return self.duration_ns / 1e9 + return None + + @duration.setter + def duration(self, value): + # type: (float) -> None + self.duration_ns = int(value * 1e9) + + def finish(self, finish_time=None): + # type: (Optional[float]) -> None + """Mark the end time of the span and submit it to the tracer. + If the span has already been finished don't do anything. + + :param finish_time: The end time of the span, in seconds. Defaults to ``now``. + """ + if self.duration_ns is not None: + return + + ft = time_ns() if finish_time is None else int(finish_time * 1e9) + # be defensive so we don't die if start isn't set + self.duration_ns = ft - (self.start_ns or ft) + + for cb in self._on_finish_callbacks: + cb(self) + + def set_tag(self, key, value=None): + # type: (_TagNameType, Any) -> None + """Set a tag key/value pair on the span. + + Keys must be strings, values must be ``stringify``-able. + + :param key: Key to use for the tag + :type key: str + :param value: Value to assign for the tag + :type value: ``stringify``-able value + """ + + if not isinstance(key, six.string_types): + log.warning("Ignoring tag pair %s:%s. Key must be a string.", key, value) + return + + # Special case, force `http.status_code` as a string + # DEV: `http.status_code` *has* to be in `meta` for metrics + # calculated in the trace agent + if key == http.STATUS_CODE: + value = str(value) + + # Determine once up front + val_is_an_int = is_integer(value) + + # Explicitly try to convert expected integers to `int` + # DEV: Some integrations parse these values from strings, but don't call `int(value)` themselves + INT_TYPES = (net.TARGET_PORT,) + if key in INT_TYPES and not val_is_an_int: + try: + value = int(value) + val_is_an_int = True + except (ValueError, TypeError): + pass + + # Set integers that are less than equal to 2^53 as metrics + if value is not None and val_is_an_int and abs(value) <= 2 ** 53: + self.set_metric(key, value) + return + + # All floats should be set as a metric + elif isinstance(value, float): + self.set_metric(key, value) + return + + # Key should explicitly be converted to a float if needed + elif key in NUMERIC_TAGS: + if value is None: + log.debug("ignoring not number metric %s:%s", key, value) + return + + try: + # DEV: `set_metric` will try to cast to `float()` for us + self.set_metric(key, value) + except (TypeError, ValueError): + log.warning("error setting numeric metric %s:%s", key, value) + + return + + elif key == MANUAL_KEEP_KEY: + self.context.sampling_priority = USER_KEEP + return + elif key == MANUAL_DROP_KEY: + self.context.sampling_priority = USER_REJECT + return + elif key == SERVICE_KEY: + self.service = value + elif key == SERVICE_VERSION_KEY: + # Also set the `version` tag to the same value + # DEV: Note that we do no return, we want to set both + self.set_tag(VERSION_KEY, value) + elif key == SPAN_MEASURED_KEY: + # Set `_dd.measured` tag as a metric + # DEV: `set_metric` will ensure it is an integer 0 or 1 + if value is None: + value = 1 + self.set_metric(key, value) + return + + try: + self.meta[key] = stringify(value) + if key in self.metrics: + del self.metrics[key] + except Exception: + log.warning("error setting tag %s, ignoring it", key, exc_info=True) + + def _set_str_tag(self, key, value): + # type: (_TagNameType, Text) -> None + """Set a value for a tag. Values are coerced to unicode in Python 2 and + str in Python 3, with decoding errors in conversion being replaced with + U+FFFD. + """ + try: + self.meta[key] = ensure_text(value, errors="replace") + except Exception as e: + if config._raise: + raise e + log.warning("Failed to set text tag '%s'", key, exc_info=True) + + def _remove_tag(self, key): + # type: (_TagNameType) -> None + if key in self.meta: + del self.meta[key] + + def get_tag(self, key): + # type: (_TagNameType) -> Optional[Text] + """Return the given tag or None if it doesn't exist.""" + return self.meta.get(key, None) + + def set_tags(self, tags): + # type: (_MetaDictType) -> None + """Set a dictionary of tags on the given span. Keys and values + must be strings (or stringable) + """ + if tags: + for k, v in iter(tags.items()): + self.set_tag(k, v) + + def set_meta(self, k, v): + # type: (_TagNameType, NumericType) -> None + self.set_tag(k, v) + + def set_metas(self, kvs): + # type: (_MetaDictType) -> None + self.set_tags(kvs) + + def set_metric(self, key, value): + # type: (_TagNameType, NumericType) -> None + # This method sets a numeric tag value for the given key. It acts + # like `set_meta()` and it simply add a tag without further processing. + + # Enforce a specific connstant for `_dd.measured` + if key == SPAN_MEASURED_KEY: + try: + value = int(bool(value)) + except (ValueError, TypeError): + log.warning("failed to convert %r tag to an integer from %r", key, value) + return + + # FIXME[matt] we could push this check to serialization time as well. + # only permit types that are commonly serializable (don't use + # isinstance so that we convert unserializable types like numpy + # numbers) + if type(value) not in numeric_types: + try: + value = float(value) + except (ValueError, TypeError): + log.debug("ignoring not number metric %s:%s", key, value) + return + + # don't allow nan or inf + if math.isnan(value) or math.isinf(value): + log.debug("ignoring not real metric %s:%s", key, value) + return + + if key in self.meta: + del self.meta[key] + self.metrics[key] = value + + def set_metrics(self, metrics): + # type: (_MetricDictType) -> None + if metrics: + for k, v in iteritems(metrics): + self.set_metric(k, v) + + def get_metric(self, key): + # type: (_TagNameType) -> Optional[NumericType] + return self.metrics.get(key) + + def to_dict(self): + # type: () -> Dict[str, Any] + d = { + "trace_id": self.trace_id, + "parent_id": self.parent_id, + "span_id": self.span_id, + "service": self.service, + "resource": self.resource, + "name": self.name, + "error": self.error, + } + + # a common mistake is to set the error field to a boolean instead of an + # int. let's special case that here, because it's sure to happen in + # customer code. + err = d.get("error") + if err and type(err) == bool: + d["error"] = 1 + + if self.start_ns: + d["start"] = self.start_ns + + if self.duration_ns: + d["duration"] = self.duration_ns + + if self.meta: + d["meta"] = self.meta + + if self.metrics: + d["metrics"] = self.metrics + + if self.span_type: + d["type"] = self.span_type + + return d + + def set_traceback(self, limit=20): + # type: (int) -> None + """If the current stack has an exception, tag the span with the + relevant error info. If not, set the span to the current python stack. + """ + (exc_type, exc_val, exc_tb) = sys.exc_info() + + if exc_type and exc_val and exc_tb: + self.set_exc_info(exc_type, exc_val, exc_tb) + else: + tb = "".join(traceback.format_stack(limit=limit + 1)[:-1]) + self.meta[ERROR_STACK] = tb + + def set_exc_info(self, exc_type, exc_val, exc_tb): + # type: (Any, Any, Any) -> None + """Tag the span with an error tuple as from `sys.exc_info()`.""" + if not (exc_type and exc_val and exc_tb): + return # nothing to do + + if self._ignored_exceptions and any([issubclass(exc_type, e) for e in self._ignored_exceptions]): # type: ignore[arg-type] # noqa + return + + self.error = 1 + + # get the traceback + buff = StringIO() + traceback.print_exception(exc_type, exc_val, exc_tb, file=buff, limit=20) + tb = buff.getvalue() + + # readable version of type (e.g. exceptions.ZeroDivisionError) + exc_type_str = "%s.%s" % (exc_type.__module__, exc_type.__name__) + + self.meta[ERROR_MSG] = str(exc_val) + self.meta[ERROR_TYPE] = exc_type_str + self.meta[ERROR_STACK] = tb + + def _remove_exc_info(self): + # type: () -> None + """Remove all exception related information from the span.""" + self.error = 0 + self._remove_tag(ERROR_MSG) + self._remove_tag(ERROR_TYPE) + self._remove_tag(ERROR_STACK) + + def pprint(self): + # type: () -> str + """Return a human readable version of the span.""" + data = [ + ("name", self.name), + ("id", self.span_id), + ("trace_id", self.trace_id), + ("parent_id", self.parent_id), + ("service", self.service), + ("resource", self.resource), + ("type", self.span_type), + ("start", self.start), + ("end", None if not self.duration else self.start + self.duration), + ("duration", self.duration), + ("error", self.error), + ("tags", dict(sorted(self.meta.items()))), + ("metrics", dict(sorted(self.metrics.items()))), + ] + return " ".join( + # use a large column width to keep pprint output on one line + "%s=%s" % (k, pprint.pformat(v, width=1024 ** 2).strip()) + for (k, v) in data + ) + + @property + def context(self): + # type: () -> Context + """Return the trace context for this span.""" + if self._context is None: + self._context = Context(trace_id=self.trace_id, span_id=self.span_id) + return self._context + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + try: + if exc_type: + self.set_exc_info(exc_type, exc_val, exc_tb) + self.finish() + except Exception: + log.exception("error closing trace") + + def __repr__(self): + return "" % ( + self.span_id, + self.trace_id, + self.parent_id, + self.name, + ) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/tracer.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/tracer.py new file mode 100644 index 000000000..1a4e43f01 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/tracer.py @@ -0,0 +1,1024 @@ +import functools +import json +import logging +import os +from os import environ +from os import getpid +import sys +from threading import RLock +from typing import Any +from typing import Callable +from typing import Dict +from typing import List +from typing import Optional +from typing import Set +from typing import TypeVar +from typing import Union + +from ddtrace import config +from ddtrace.filters import TraceFilter +from ddtrace.utils.deprecation import deprecation +from ddtrace.vendor import debtcollector + +from . import _hooks +from .constants import AUTO_KEEP +from .constants import AUTO_REJECT +from .constants import ENV_KEY +from .constants import FILTERS_KEY +from .constants import HOSTNAME_KEY +from .constants import PID +from .constants import SAMPLE_RATE_METRIC_KEY +from .constants import VERSION_KEY +from .context import Context +from .internal import agent +from .internal import atexit +from .internal import compat +from .internal import debug +from .internal import forksafe +from .internal import hostname +from .internal import service +from .internal.dogstatsd import get_dogstatsd_client +from .internal.logger import get_logger +from .internal.logger import hasHandlers +from .internal.processor import SpanProcessor +from .internal.processor.trace import SpanAggregator +from .internal.processor.trace import TraceProcessor +from .internal.processor.trace import TraceSamplingProcessor +from .internal.processor.trace import TraceTagsProcessor +from .internal.runtime import get_runtime_id +from .internal.writer import AgentWriter +from .internal.writer import LogWriter +from .internal.writer import TraceWriter +from .monkey import patch +from .provider import DefaultContextProvider +from .sampler import BasePrioritySampler +from .sampler import BaseSampler +from .sampler import DatadogSampler +from .sampler import RateByServiceSampler +from .sampler import RateSampler +from .span import Span +from .utils.deprecation import deprecated +from .utils.formats import asbool +from .utils.formats import get_env + + +log = get_logger(__name__) + +debug_mode = asbool(get_env("trace", "debug", default=False)) +call_basic_config = asbool(os.environ.get("DD_CALL_BASIC_CONFIG", "true")) + +DD_LOG_FORMAT = "%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] {}- %(message)s".format( + "[dd.service=%(dd.service)s dd.env=%(dd.env)s dd.version=%(dd.version)s" + " dd.trace_id=%(dd.trace_id)s dd.span_id=%(dd.span_id)s] " +) +if debug_mode and not hasHandlers(log) and call_basic_config: + if config.logs_injection: + # We need to ensure logging is patched in case the tracer logs during initialization + patch(logging=True) + logging.basicConfig(level=logging.DEBUG, format=DD_LOG_FORMAT) + else: + logging.basicConfig(level=logging.DEBUG) + + +_INTERNAL_APPLICATION_SPAN_TYPES = {"custom", "template", "web", "worker"} + + +AnyCallable = TypeVar("AnyCallable", bound=Callable) + + +class Tracer(object): + """ + Tracer is used to create, sample and submit spans that measure the + execution time of sections of code. + + If you're running an application that will serve a single trace per thread, + you can use the global tracer instance:: + + from ddtrace import tracer + trace = tracer.trace('app.request', 'web-server').finish() + """ + + SHUTDOWN_TIMEOUT = 5 + + def __init__( + self, + url=None, # type: Optional[str] + dogstatsd_url=None, # type: Optional[str] + ): + # type: (...) -> None + """ + Create a new ``Tracer`` instance. A global tracer is already initialized + for common usage, so there is no need to initialize your own ``Tracer``. + + :param url: The Datadog agent URL. + :param dogstatsd_url: The DogStatsD URL. + """ + self.log = log + self._filters = [] # type: List[TraceFilter] + + # globally set tags + self.tags = config.tags.copy() + + # a buffer for service info so we don't perpetually send the same things + self._services = set() # type: Set[str] + + # Runtime id used for associating data collected during runtime to + # traces + self._pid = getpid() + + self.enabled = asbool(get_env("trace", "enabled", default=True)) + self.context_provider = DefaultContextProvider() + self.sampler = DatadogSampler() # type: BaseSampler + self.priority_sampler = RateByServiceSampler() # type: Optional[BasePrioritySampler] + self._dogstatsd_url = agent.get_stats_url() if dogstatsd_url is None else dogstatsd_url + + if self._use_log_writer() and url is None: + writer = LogWriter() # type: TraceWriter + else: + url = url or agent.get_trace_url() + agent.verify_url(url) + + writer = AgentWriter( + agent_url=url, + sampler=self.sampler, + priority_sampler=self.priority_sampler, + dogstatsd=get_dogstatsd_client(self._dogstatsd_url), + report_metrics=config.health_metrics_enabled, + sync_mode=self._use_sync_mode(), + ) + self.writer = writer # type: TraceWriter + + # DD_TRACER_... should be deprecated after version 1.0.0 is released + pfe_default_value = False + pfms_default_value = 500 + if "DD_TRACER_PARTIAL_FLUSH_ENABLED" in os.environ or "DD_TRACER_PARTIAL_FLUSH_MIN_SPANS" in os.environ: + deprecation("DD_TRACER_... use DD_TRACE_... instead", version="1.0.0") + pfe_default_value = asbool(get_env("tracer", "partial_flush_enabled", default=pfe_default_value)) + pfms_default_value = int( + get_env("tracer", "partial_flush_min_spans", default=pfms_default_value) # type: ignore[arg-type] + ) + self._partial_flush_enabled = asbool(get_env("trace", "partial_flush_enabled", default=pfe_default_value)) + self._partial_flush_min_spans = int( + get_env("trace", "partial_flush_min_spans", default=pfms_default_value) # type: ignore[arg-type] + ) + + self._initialize_span_processors() + self._hooks = _hooks.Hooks() + atexit.register(self._atexit) + forksafe.register(self._child_after_fork) + + self._shutdown_lock = RLock() + + self._new_process = False + + def _atexit(self): + # type: () -> None + key = "ctrl-break" if os.name == "nt" else "ctrl-c" + log.debug( + "Waiting %d seconds for tracer to finish. Hit %s to quit.", + self.SHUTDOWN_TIMEOUT, + key, + ) + self.shutdown(timeout=self.SHUTDOWN_TIMEOUT) + + def on_start_span(self, func): + # type: (Callable) -> Callable + """Register a function to execute when a span start. + + Can be used as a decorator. + + :param func: The function to call when starting a span. + The started span will be passed as argument. + """ + self._hooks.register(self.__class__.start_span, func) + return func + + def deregister_on_start_span(self, func): + # type: (Callable) -> Callable + """Unregister a function registered to execute when a span starts. + + Can be used as a decorator. + + :param func: The function to stop calling when starting a span. + """ + + self._hooks.deregister(self.__class__.start_span, func) + return func + + @property + def debug_logging(self): + return self.log.isEnabledFor(logging.DEBUG) + + @debug_logging.setter # type: ignore[misc] + @deprecated(message="Use logging.setLevel instead", version="1.0.0") + def debug_logging(self, value): + # type: (bool) -> None + self.log.setLevel(logging.DEBUG if value else logging.WARN) + + @deprecated("Use .tracer, not .tracer()", "1.0.0") + def __call__(self): + return self + + @deprecated("This method will be removed altogether", "1.0.0") + def global_excepthook(self, tp, value, traceback): + """The global tracer except hook.""" + + @deprecated( + "Call context has been superseded by trace context. Please use current_trace_context() instead.", "1.0.0" + ) + def get_call_context(self, *args, **kwargs): + # type: (...) -> Context + """ + Return the current active ``Context`` for this traced execution. This method is + automatically called in the ``tracer.trace()``, but it can be used in the application + code during manual instrumentation like:: + + from ddtrace import tracer + + async def web_handler(request): + context = tracer.get_call_context() + # use the context if needed + # ... + + This method makes use of a ``ContextProvider`` that is automatically set during the tracer + initialization, or while using a library instrumentation. + """ + ctx = self.current_trace_context(*args, **kwargs) + if ctx is None: + ctx = Context() + return ctx + + def current_trace_context(self, *args, **kwargs): + # type: (...) -> Optional[Context] + """Return the context for the current trace. + + If there is no active trace then None is returned. + """ + active = self.context_provider.active() + if isinstance(active, Context): + return active + elif isinstance(active, Span): + return active.context + return None + + def get_log_correlation_context(self): + # type: () -> Dict[str, str] + """Retrieves the data used to correlate a log with the current active trace. + Generates a dictionary for custom logging instrumentation including the trace id and + span id of the current active span, as well as the configured service, version, and environment names. + If there is no active span, a dictionary with an empty string for each value will be returned. + """ + span = None + if self.enabled: + span = self.current_span() + + return { + "trace_id": str(span.trace_id) if span else "0", + "span_id": str(span.span_id) if span else "0", + "service": config.service or "", + "version": config.version or "", + "env": config.env or "", + } + + # TODO: deprecate this method and make sure users create a new tracer if they need different parameters + def configure( + self, + enabled=None, # type: Optional[bool] + hostname=None, # type: Optional[str] + port=None, # type: Optional[int] + uds_path=None, # type: Optional[str] + https=None, # type: Optional[bool] + sampler=None, # type: Optional[BaseSampler] + context_provider=None, # type: Optional[DefaultContextProvider] + wrap_executor=None, # type: Optional[Callable] + priority_sampling=None, # type: Optional[bool] + settings=None, # type: Optional[Dict[str, Any]] + dogstatsd_url=None, # type: Optional[str] + writer=None, # type: Optional[TraceWriter] + partial_flush_enabled=None, # type: Optional[bool] + partial_flush_min_spans=None, # type: Optional[int] + ): + # type: (...) -> None + """ + Configure an existing Tracer the easy way. + Allow to configure or reconfigure a Tracer instance. + + :param bool enabled: If True, finished traces will be submitted to the API. + Otherwise they'll be dropped. + :param str hostname: Hostname running the Trace Agent + :param int port: Port of the Trace Agent + :param str uds_path: The Unix Domain Socket path of the agent. + :param bool https: Whether to use HTTPS or HTTP. + :param object sampler: A custom Sampler instance, locally deciding to totally drop the trace or not. + :param object context_provider: The ``ContextProvider`` that will be used to retrieve + automatically the current call context. This is an advanced option that usually + doesn't need to be changed from the default value + :param object wrap_executor: callable that is used when a function is decorated with + ``Tracer.wrap()``. This is an advanced option that usually doesn't need to be changed + from the default value + :param priority_sampling: enable priority sampling, this is required for + complete distributed tracing support. Enabled by default. + :param str dogstatsd_url: URL for UDP or Unix socket connection to DogStatsD + """ + if enabled is not None: + self.enabled = enabled + + if settings is not None: + filters = settings.get(FILTERS_KEY) + if filters is not None: + self._filters = filters + + if partial_flush_enabled is not None: + self._partial_flush_enabled = partial_flush_enabled + + if partial_flush_min_spans is not None: + self._partial_flush_min_spans = partial_flush_min_spans + + # If priority sampling is not set or is True and no priority sampler is set yet + if priority_sampling in (None, True) and not self.priority_sampler: + self.priority_sampler = RateByServiceSampler() + # Explicitly disable priority sampling + elif priority_sampling is False: + self.priority_sampler = None + + if sampler is not None: + self.sampler = sampler + + self._dogstatsd_url = dogstatsd_url or self._dogstatsd_url + + if any(x is not None for x in [hostname, port, uds_path, https]): + # If any of the parts of the URL have updated, merge them with + # the previous writer values. + if isinstance(self.writer, AgentWriter): + prev_url_parsed = compat.parse.urlparse(self.writer.agent_url) + else: + prev_url_parsed = compat.parse.urlparse("") + + if uds_path is not None: + if hostname is None and prev_url_parsed.scheme == "unix": + hostname = prev_url_parsed.hostname + url = "unix://%s%s" % (hostname or "", uds_path) + else: + if https is None: + https = prev_url_parsed.scheme == "https" + if hostname is None: + hostname = prev_url_parsed.hostname or "" + if port is None: + port = prev_url_parsed.port + scheme = "https" if https else "http" + url = "%s://%s:%s" % (scheme, hostname, port) + elif isinstance(self.writer, AgentWriter): + # Reuse the URL from the previous writer if there was one. + url = self.writer.agent_url + else: + # No URL parts have updated and there's no previous writer to + # get the URL from. + url = None # type: ignore + + try: + self.writer.stop() + except service.ServiceStatusError: + # It's possible the writer never got started in the first place :( + pass + + if writer is not None: + self.writer = writer + elif url: + # Verify the URL and create a new AgentWriter with it. + agent.verify_url(url) + self.writer = AgentWriter( + url, + sampler=self.sampler, + priority_sampler=self.priority_sampler, + dogstatsd=get_dogstatsd_client(self._dogstatsd_url), + report_metrics=config.health_metrics_enabled, + sync_mode=self._use_sync_mode(), + ) + elif writer is None and isinstance(self.writer, LogWriter): + # No need to do anything for the LogWriter. + pass + if isinstance(self.writer, AgentWriter): + self.writer.dogstatsd = get_dogstatsd_client(self._dogstatsd_url) # type: ignore[has-type] + self._initialize_span_processors() + + if context_provider is not None: + self.context_provider = context_provider + + if wrap_executor is not None: + self._wrap_executor = wrap_executor + + if debug_mode or asbool(environ.get("DD_TRACE_STARTUP_LOGS", False)): + try: + info = debug.collect(self) + except Exception as e: + msg = "Failed to collect start-up logs: %s" % e + self._log_compat(logging.WARNING, "- DATADOG TRACER DIAGNOSTIC - %s" % msg) + else: + if self.log.isEnabledFor(logging.INFO): + msg = "- DATADOG TRACER CONFIGURATION - %s" % json.dumps(info) + self._log_compat(logging.INFO, msg) + + # Always log errors since we're either in debug_mode or start up logs + # are enabled. + agent_error = info.get("agent_error") + if agent_error: + msg = "- DATADOG TRACER DIAGNOSTIC - %s" % agent_error + self._log_compat(logging.WARNING, msg) + + def _child_after_fork(self): + self._pid = getpid() + + # Assume that the services of the child are not necessarily a subset of those + # of the parent. + self._services = set() + + # Re-create the background writer thread + self.writer = self.writer.recreate() + self._initialize_span_processors() + + self._new_process = True + + def _start_span( + self, + name, # type: str + child_of=None, # type: Optional[Union[Span, Context]] + service=None, # type: Optional[str] + resource=None, # type: Optional[str] + span_type=None, # type: Optional[str] + activate=False, # type: bool + ): + # type: (...) -> Span + """Return a span that represents an operation called ``name``. + + Note that the :meth:`.trace` method will almost always be preferred + over this method as it provides automatic span parenting. This method + should only be used if manual parenting is desired. + + :param str name: the name of the operation being traced. + :param object child_of: a ``Span`` or a ``Context`` instance representing the parent for this span. + :param str service: the name of the service being traced. + :param str resource: an optional name of the resource being tracked. + :param str span_type: an optional operation type. + :param activate: activate the span once it is created. + + To start a new root span:: + + span = tracer.start_span("web.request") + + To create a child for a root span:: + + root_span = tracer.start_span("web.request") + span = tracer.start_span("web.decoder", child_of=root_span) + + Spans from ``start_span`` are not activated by default:: + + with tracer.start_span("parent") as parent: + assert tracer.current_span() is None + with tracer.start_span("child", child_of=parent): + assert tracer.current_span() is None + + new_parent = tracer.start_span("new_parent", activate=True) + assert tracer.current_span() is new_parent + + Note: be sure to finish all spans to avoid memory leaks and incorrect + parenting of spans. + """ + if self._new_process: + self._new_process = False + + # The spans remaining in the context can not and will not be + # finished in this new process. So to avoid memory leaks the + # strong span reference (which will never be finished) is replaced + # with a context representing the span. + if isinstance(child_of, Span): + new_ctx = Context( + sampling_priority=child_of.context.sampling_priority, + span_id=child_of.span_id, + trace_id=child_of.trace_id, + ) + + # If the child_of span was active then activate the new context + # containing it so that the strong span referenced is removed + # from the execution. + if self.context_provider.active() is child_of: + self.context_provider.activate(new_ctx) + child_of = new_ctx + + parent = None # type: Optional[Span] + if child_of is not None: + if isinstance(child_of, Context): + context = child_of + else: + context = child_of.context + parent = child_of + else: + context = Context() + + if parent: + trace_id = parent.trace_id # type: Optional[int] + parent_id = parent.span_id # type: Optional[int] + else: + trace_id = context.trace_id + parent_id = context.span_id + + # The following precedence is used for a new span's service: + # 1. Explicitly provided service name + # a. User provided or integration provided service name + # 2. Parent's service name (if defined) + # 3. Globally configured service name + # a. `config.service`/`DD_SERVICE`/`DD_TAGS` + if service is None: + if parent: + service = parent.service + else: + service = config.service + + mapped_service = config.service_mapping.get(service, service) + + if trace_id: + # child_of a non-empty context, so either a local child span or from a remote context + span = Span( + self, + name, + context=context, + trace_id=trace_id, + parent_id=parent_id, + service=mapped_service, + resource=resource, + span_type=span_type, + on_finish=[self._on_span_finish], + ) + + # Extra attributes when from a local parent + if parent: + span.sampled = parent.sampled + span._parent = parent + span._local_root = parent._local_root + + if span._local_root is None: + span._local_root = span + else: + # this is the root span of a new trace + span = Span( + self, + name, + context=context, + service=mapped_service, + resource=resource, + span_type=span_type, + on_finish=[self._on_span_finish], + ) + span._local_root = span + if config.report_hostname: + span.meta[HOSTNAME_KEY] = hostname.get_hostname() + span.sampled = self.sampler.sample(span) + # Old behavior + # DEV: The new sampler sets metrics and priority sampling on the span for us + if not isinstance(self.sampler, DatadogSampler): + if span.sampled: + # When doing client sampling in the client, keep the sample rate so that we can + # scale up statistics in the next steps of the pipeline. + if isinstance(self.sampler, RateSampler): + span.set_metric(SAMPLE_RATE_METRIC_KEY, self.sampler.sample_rate) + + if self.priority_sampler: + # At this stage, it's important to have the service set. If unset, + # priority sampler will use the default sampling rate, which might + # lead to oversampling (that is, dropping too many traces). + if self.priority_sampler.sample(span): + context.sampling_priority = AUTO_KEEP + else: + context.sampling_priority = AUTO_REJECT + else: + if self.priority_sampler: + # If dropped by the local sampler, distributed instrumentation can drop it too. + context.sampling_priority = AUTO_REJECT + else: + context.sampling_priority = AUTO_KEEP if span.sampled else AUTO_REJECT + # We must always mark the span as sampled so it is forwarded to the agent + span.sampled = True + + if not span._parent: + span.meta["runtime-id"] = get_runtime_id() + span.metrics[PID] = self._pid + + # Apply default global tags. + if self.tags: + span.set_tags(self.tags) + + if config.env: + span._set_str_tag(ENV_KEY, config.env) + + # Only set the version tag on internal spans. + if config.version: + root_span = self.current_root_span() + # if: 1. the span is the root span and the span's service matches the global config; or + # 2. the span is not the root, but the root span's service matches the span's service + # and the root span has a version tag + # then the span belongs to the user application and so set the version tag + if (root_span is None and service == config.service) or ( + root_span and root_span.service == service and VERSION_KEY in root_span.meta + ): + span._set_str_tag(VERSION_KEY, config.version) + + if activate: + self.context_provider.activate(span) + + # update set of services handled by tracer + if service and service not in self._services and self._is_span_internal(span): + self._services.add(service) + + # Only call span processors if the tracer is enabled + if self.enabled: + for p in self._span_processors: + p.on_span_start(span) + + self._hooks.emit(self.__class__.start_span, span) + return span + + start_span = _start_span + + def _on_span_finish(self, span): + # type: (Span) -> None + active = self.current_span() + # Debug check: if the finishing span has a parent and its parent + # is not the next active span then this is an error in synchronous tracing. + if span._parent is not None and active is not span._parent: + self.log.debug( + "span %r closing after its parent %r, this is an error when not using async", span, span._parent + ) + + # Only call span processors if the tracer is enabled + if self.enabled: + for p in self._span_processors: + p.on_span_finish(span) + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("finishing span %s (enabled:%s)", span.pprint(), self.enabled) + + def _initialize_span_processors(self): + # type: () -> None + trace_processors = [] # type: List[TraceProcessor] + trace_processors += [TraceTagsProcessor()] + trace_processors += [TraceSamplingProcessor()] + trace_processors += self._filters + + self._span_processors = [ + SpanAggregator( + partial_flush_enabled=self._partial_flush_enabled, + partial_flush_min_spans=self._partial_flush_min_spans, + trace_processors=trace_processors, + writer=self.writer, + ), + ] # type: List[SpanProcessor] + + def _log_compat(self, level, msg): + """Logs a message for the given level. + + Python 2 will not submit logs to stderr if no handler is configured. + + Instead, something like this will be printed to stderr: + No handlers could be found for logger "ddtrace.tracer" + + Since the global tracer is configured on import and it is recommended + to import the tracer as early as possible, it will likely be the case + that there are no handlers installed yet. + """ + if compat.PY2 and not hasHandlers(self.log): + sys.stderr.write("%s\n" % msg) + else: + self.log.log(level, msg) + + def _trace(self, name, service=None, resource=None, span_type=None): + # type: (str, Optional[str], Optional[str], Optional[str]) -> Span + """Activate and return a new span that inherits from the current active span. + + :param str name: the name of the operation being traced + :param str service: the name of the service being traced. If not set, + it will inherit the service from its parent. + :param str resource: an optional name of the resource being tracked. + :param str span_type: an optional operation type. + + The returned span *must* be ``finish``'d or it will remain in memory + indefinitely:: + + >>> span = tracer.trace("web.request") + try: + # do something + finally: + span.finish() + + >>> with tracer.trace("web.request") as span: + # do something + + Example of the automatic parenting:: + + parent = tracer.trace("parent") # has no parent span + assert tracer.current_span() is parent + + child = tracer.trace("child") + assert child.parent_id == parent.span_id + assert tracer.current_span() is child + child.finish() + + # parent is now the active span again + assert tracer.current_span() is parent + parent.finish() + + assert tracer.current_span() is None + + parent2 = tracer.trace("parent2") + assert parent2.parent_id is None + parent2.finish() + """ + return self.start_span( + name, + child_of=self.context_provider.active(), + service=service, + resource=resource, + span_type=span_type, + activate=True, + ) + + trace = _trace + + def current_root_span(self): + # type: () -> Optional[Span] + """Returns the root span of the current execution. + + This is useful for attaching information related to the trace as a + whole without needing to add to child spans. + + For example:: + + # get the root span + root_span = tracer.current_root_span() + # set the host just once on the root span + if root_span: + root_span.set_tag('host', '127.0.0.1') + """ + span = self.current_span() + if span is None: + return None + return span._local_root + + def current_span(self): + # type: () -> Optional[Span] + """Return the active span in the current execution context. + + Note that there may be an active span represented by a context object + (like from a distributed trace) which will not be returned by this + method. + """ + active = self.context_provider.active() + return active if isinstance(active, Span) else None + + def write(self, spans): + # type: (Optional[List[Span]]) -> None + """ + Send the trace to the writer to enqueue the spans list in the agent + sending queue. + """ + if not spans: + return # nothing to do + + if self.log.isEnabledFor(logging.DEBUG): + self.log.debug("writing %s spans (enabled:%s)", len(spans), self.enabled) + for span in spans: + self.log.debug("\n%s", span.pprint()) + + if not self.enabled: + return + + if spans is not None: + self.writer.write(spans=spans) + + @deprecated(message="Manually setting service info is no longer necessary", version="1.0.0") + def set_service_info(self, *args, **kwargs): + """Set the information about the given service.""" + return + + def wrap( + self, + name=None, # type: Optional[str] + service=None, # type: Optional[str] + resource=None, # type: Optional[str] + span_type=None, # type: Optional[str] + ): + # type: (...) -> Callable[[AnyCallable], AnyCallable] + """ + A decorator used to trace an entire function. If the traced function + is a coroutine, it traces the coroutine execution when is awaited. + If a ``wrap_executor`` callable has been provided in the ``Tracer.configure()`` + method, it will be called instead of the default one when the function + decorator is invoked. + + :param str name: the name of the operation being traced. If not set, + defaults to the fully qualified function name. + :param str service: the name of the service being traced. If not set, + it will inherit the service from it's parent. + :param str resource: an optional name of the resource being tracked. + :param str span_type: an optional operation type. + + >>> @tracer.wrap('my.wrapped.function', service='my.service') + def run(): + return 'run' + + >>> # name will default to 'execute' if unset + @tracer.wrap() + def execute(): + return 'executed' + + >>> # or use it in asyncio coroutines + @tracer.wrap() + async def coroutine(): + return 'executed' + + >>> @tracer.wrap() + @asyncio.coroutine + def coroutine(): + return 'executed' + + You can access the current span using `tracer.current_span()` to set + tags: + + >>> @tracer.wrap() + def execute(): + span = tracer.current_span() + span.set_tag('a', 'b') + """ + + def wrap_decorator(f): + # type: (AnyCallable) -> AnyCallable + # FIXME[matt] include the class name for methods. + span_name = name if name else "%s.%s" % (f.__module__, f.__name__) + + # detect if the the given function is a coroutine to use the + # right decorator; this initial check ensures that the + # evaluation is done only once for each @tracer.wrap + if compat.iscoroutinefunction(f): + # call the async factory that creates a tracing decorator capable + # to await the coroutine execution before finishing the span. This + # code is used for compatibility reasons to prevent Syntax errors + # in Python 2 + func_wrapper = compat.make_async_decorator( + self, + f, + span_name, + service=service, + resource=resource, + span_type=span_type, + ) + else: + + @functools.wraps(f) + def func_wrapper(*args, **kwargs): + # if a wrap executor has been configured, it is used instead + # of the default tracing function + if getattr(self, "_wrap_executor", None): + return self._wrap_executor( + self, + f, + args, + kwargs, + span_name, + service=service, + resource=resource, + span_type=span_type, + ) + + # otherwise fallback to a default tracing + with self.trace(span_name, service=service, resource=resource, span_type=span_type): + return f(*args, **kwargs) + + return func_wrapper + + return wrap_decorator + + def set_tags(self, tags): + # type: (Dict[str, str]) -> None + """Set some tags at the tracer level. + This will append those tags to each span created by the tracer. + + :param dict tags: dict of tags to set at tracer level + """ + self.tags.update(tags) + + def _restore_from_shutdown(self): + with self._shutdown_lock: + if self.start_span is self._start_span: + # Already restored + return + + atexit.register(self._atexit) + forksafe.register(self._child_after_fork) + + self.start_span = self._start_span + self.trace = self._trace + + debtcollector.deprecate( + "Tracing with a tracer that has been shut down is being deprecated. " + "A new tracer should be created for generating new traces", + version="1.0.0", + ) + + def _shutdown_start_span( + self, + name, # type: str + child_of=None, # type: Optional[Union[Span, Context]] + service=None, # type: Optional[str] + resource=None, # type: Optional[str] + span_type=None, # type: Optional[str] + activate=False, # type: bool + ): + # type: (...) -> Span + self._restore_from_shutdown() + + return self.start_span(name, child_of, service, resource, span_type, activate) + + def _shutdown_trace(self, name, service=None, resource=None, span_type=None): + # type: (str, Optional[str], Optional[str], Optional[str]) -> Span + self._restore_from_shutdown() + + return self.trace(name, service, resource, span_type) + + def shutdown(self, timeout=None): + # type: (Optional[float]) -> None + """Shutdown the tracer. + + This will stop the background writer/worker and flush any finished traces in the buffer. The tracer cannot be + used for tracing after this method has been called. A new tracer instance is required to continue tracing. + + :param timeout: How long in seconds to wait for the background worker to flush traces + before exiting or :obj:`None` to block until flushing has successfully completed (default: :obj:`None`) + :type timeout: :obj:`int` | :obj:`float` | :obj:`None` + """ + try: + self.writer.stop(timeout=timeout) + except service.ServiceStatusError: + # It's possible the writer never got started in the first place :( + pass + + with self._shutdown_lock: + atexit.unregister(self._atexit) + forksafe.unregister(self._child_after_fork) + + self.start_span = self._shutdown_start_span # type: ignore[assignment] + self.trace = self._shutdown_trace # type: ignore[assignment] + + @staticmethod + def _use_log_writer(): + # type: () -> bool + """Returns whether the LogWriter should be used in the environment by + default. + + The LogWriter required by default in AWS Lambdas when the Datadog Agent extension + is not available in the Lambda. + """ + if ( + environ.get("DD_AGENT_HOST") + or environ.get("DATADOG_TRACE_AGENT_HOSTNAME") + or environ.get("DD_TRACE_AGENT_URL") + ): + # If one of these variables are set, we definitely have an agent + return False + elif _in_aws_lambda() and _has_aws_lambda_agent_extension(): + # If the Agent Lambda extension is available then an AgentWriter is used. + return False + else: + return _in_aws_lambda() + + @staticmethod + def _use_sync_mode(): + # type: () -> bool + """Returns, if an `AgentWriter` is to be used, whether it should be run + in synchronous mode by default. + + There is only one case in which this is desirable: + + - AWS Lambdas can have the Datadog agent installed via an extension. + When it's available traces must be sent synchronously to ensure all + are received before the Lambda terminates. + """ + return _in_aws_lambda() and _has_aws_lambda_agent_extension() + + @staticmethod + def _is_span_internal(span): + return not span.span_type or span.span_type in _INTERNAL_APPLICATION_SPAN_TYPES + + +def _has_aws_lambda_agent_extension(): + # type: () -> bool + """Returns whether the environment has the AWS Lambda Datadog Agent + extension available. + """ + return os.path.exists("/opt/extensions/datadog-agent") + + +def _in_aws_lambda(): + # type: () -> bool + """Returns whether the environment is an AWS Lambda. + This is accomplished by checking if the AWS_LAMBDA_FUNCTION_NAME environment + variable is defined. + """ + return bool(environ.get("AWS_LAMBDA_FUNCTION_NAME", False)) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/util.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/util.py new file mode 100644 index 000000000..6dea16a0a --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/util.py @@ -0,0 +1,24 @@ +# [Backward compatibility]: keep importing modules functions +from .utils.deprecation import deprecated +from .utils.deprecation import deprecation +from .utils.formats import asbool +from .utils.formats import deep_getattr +from .utils.formats import get_env +from .utils.wrappers import safe_patch +from .utils.wrappers import unwrap + + +deprecation( + name="ddtrace.util", + message="Use `ddtrace.utils` package instead", + version="1.0.0", +) + +__all__ = [ + "deprecated", + "asbool", + "deep_getattr", + "get_env", + "safe_patch", + "unwrap", +] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__init__.py new file mode 100644 index 000000000..036f580d5 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__init__.py @@ -0,0 +1,41 @@ +from typing import Any +from typing import Dict +from typing import List +from typing import Optional + + +class ArgumentError(Exception): + """ + This is raised when an argument lookup, either by position or by keyword, is + not found. + """ + + +def get_argument_value( + args, # type: List[Any] + kwargs, # type: Dict[str, Any] + pos, # type: int + kw, # type: str +): + # type: (...) -> Optional[Any] + """ + This function parses the value of a target function argument that may have been + passed in as a positional argument or a keyword argument. Because monkey-patched + functions do not define the same signature as their target function, the value of + arguments must be inferred from the packed args and kwargs. + Keyword arguments are prioritized, followed by the positional argument. If the + argument cannot be resolved, an ``ArgumentError`` exception is raised, which could + be used, e.g., to handle a default value by the caller. + :param args: Positional arguments + :param kwargs: Keyword arguments + :param pos: The positional index of the argument if passed in as a positional arg + :param kw: The name of the keyword if passed in as a keyword argument + :return: The value of the target argument + """ + try: + return kwargs[kw] + except KeyError: + try: + return args[pos] + except IndexError: + raise ArgumentError("%s (at position %d)" % (kw, pos)) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..b4dfe50a3 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/attr.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/attr.cpython-38.pyc new file mode 100644 index 000000000..18a4d14f6 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/attr.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/attrdict.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/attrdict.cpython-38.pyc new file mode 100644 index 000000000..477878c7e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/attrdict.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/cache.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/cache.cpython-38.pyc new file mode 100644 index 000000000..e7e23b5fb Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/cache.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/config.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/config.cpython-38.pyc new file mode 100644 index 000000000..dcac714ef Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/config.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/deprecation.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/deprecation.cpython-38.pyc new file mode 100644 index 000000000..92f08b71e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/deprecation.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/formats.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/formats.cpython-38.pyc new file mode 100644 index 000000000..e08dffe22 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/formats.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/http.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/http.cpython-38.pyc new file mode 100644 index 000000000..831e5bccc Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/http.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/importlib.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/importlib.cpython-38.pyc new file mode 100644 index 000000000..d7d867fad Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/importlib.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/time.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/time.cpython-38.pyc new file mode 100644 index 000000000..e25f2d9ae Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/time.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/version.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/version.cpython-38.pyc new file mode 100644 index 000000000..06e967f2b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/version.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/wrappers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/wrappers.cpython-38.pyc new file mode 100644 index 000000000..ebac0a3dc Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/__pycache__/wrappers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/attr.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/attr.py new file mode 100644 index 000000000..f491ff9e6 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/attr.py @@ -0,0 +1,16 @@ +import os +from typing import Callable +from typing import Type +from typing import TypeVar +from typing import Union + + +T = TypeVar("T") + + +def from_env(name, default, value_type): + # type: (str, T, Union[Callable[[Union[str, T, None]], T], Type[T]]) -> Callable[[], T] + def _(): + return value_type(os.environ.get(name, default)) + + return _ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/attrdict.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/attrdict.py new file mode 100644 index 000000000..7b17cf945 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/attrdict.py @@ -0,0 +1,42 @@ +from typing import Any + + +class AttrDict(dict): + """ + dict implementation that allows for item attribute access + + + Example:: + + data = AttrDict() + data['key'] = 'value' + print(data['key']) + + data.key = 'new-value' + print(data.key) + + # Convert an existing `dict` + data = AttrDict(dict(key='value')) + print(data.key) + """ + + def __getattr__(self, key): + # type: (str) -> Any + if key in self: + return self[key] + return object.__getattribute__(self, key) + + def __setattr__(self, key, value): + # type: (str, Any) -> None + # 1) Ensure if the key exists from a dict key we always prefer that + # 2) If we do not have an existing key but we do have an attr, set that + # 3) No existing key or attr exists, so set a key + if key in self: + # Update any existing key + self[key] = value + elif hasattr(self, key): + # Allow overwriting an existing attribute, e.g. `self.global_config = dict()` + object.__setattr__(self, key, value) + else: + # Set a new key + self[key] = value diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/cache.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/cache.py new file mode 100644 index 000000000..02e0f9801 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/cache.py @@ -0,0 +1,85 @@ +from threading import RLock +from typing import Any +from typing import Callable +from typing import Dict +from typing import Optional +from typing import Tuple +from typing import Type +from typing import TypeVar + + +miss = object() + +T = TypeVar("T") +S = TypeVar("S") +F = Callable[[T], S] +M = Callable[[Any, T], S] + + +def cached(maxsize=256): + # type: (int) -> Callable[[F], F] + """ + Decorator for caching the result of functions with a single argument. + + The strategy is MFU, meaning that only the most frequently used values are + retained. The amortized cost of shrinking the cache when it grows beyond + the requested size is O(log(size)). + """ + + def cached_wrapper(f): + # type: (F) -> F + cache = {} # type: Dict[Any, Tuple[Any, int]] + lock = RLock() + + def cached_f(key): + # type: (T) -> S + if len(cache) >= maxsize: + for _, h in zip(range(maxsize >> 1), sorted(cache, key=lambda h: cache[h][1])): + del cache[h] + + _ = cache.get(key, miss) + if _ is not miss: + value, count = _ + cache[key] = (value, count + 1) + return value + + with lock: + _ = cache.get(key, miss) + if _ is not miss: + value, count = _ + cache[key] = (value, count + 1) + return value + + result = f(key) + + cache[key] = (result, 1) + + return result + + cached_f.invalidate = cache.clear # type: ignore[attr-defined] + + return cached_f + + return cached_wrapper + + +class CachedMethodDescriptor(object): + def __init__(self, method, maxsize): + # type: (M, int) -> None + self._method = method + self._maxsize = maxsize + + def __get__(self, obj, objtype=None): + # type: (Any, Optional[Type]) -> F + cached_method = cached(self._maxsize)(self._method.__get__(obj, objtype)) # type: ignore[attr-defined] + setattr(obj, self._method.__name__, cached_method) + return cached_method + + +def cachedmethod(maxsize=256): + # type: (int) -> Callable[[M], CachedMethodDescriptor] + def cached_wrapper(f): + # type: (M) -> CachedMethodDescriptor + return CachedMethodDescriptor(f, maxsize) + + return cached_wrapper diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/config.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/config.py new file mode 100644 index 000000000..7d533b8a6 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/config.py @@ -0,0 +1,19 @@ +import os +import sys +import typing + + +def get_application_name(): + # type: () -> typing.Optional[str] + """Attempts to find the application name using system arguments.""" + try: + import __main__ + + name = __main__.__file__ + except (ImportError, AttributeError): + try: + name = sys.argv[0] + except (AttributeError, IndexError): + return None + + return os.path.basename(name) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/deprecation.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/deprecation.py new file mode 100644 index 000000000..9e9197c27 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/deprecation.py @@ -0,0 +1,101 @@ +from functools import wraps +import os +from typing import Any +from typing import Callable +from typing import Dict +from typing import Optional +from typing import Tuple +import warnings + +from ddtrace.vendor import debtcollector + + +class RemovedInDDTrace10Warning(DeprecationWarning): + pass + + +def format_message(name, message, version=None): + # type: (str, str, Optional[str]) -> str + """Message formatter to create `DeprecationWarning` messages + such as: + + 'fn' is deprecated and will be remove in future versions (1.0). + """ + return "'{}' is deprecated and will be remove in future versions{}. {}".format( + name, + " ({})".format(version) if version is not None else "", + message, + ) + + +def warn(message, stacklevel=2): + # type: (str, int) -> None + """Helper function used as a ``DeprecationWarning``.""" + warnings.warn(message, RemovedInDDTrace10Warning, stacklevel=stacklevel) + + +def deprecation(name="", message="", version=None): + # type: (str, str, Optional[str]) -> None + """Function to report a ``DeprecationWarning``. Bear in mind that `DeprecationWarning` + are ignored by default so they're not available in user logs. To show them, + the application must be launched with a special flag: + + $ python -Wall script.py + + This approach is used by most of the frameworks, including Django + (ref: https://docs.djangoproject.com/en/2.0/howto/upgrade-version/#resolving-deprecation-warnings) + """ + msg = format_message(name, message, version) + warn(msg, stacklevel=4) + + +def deprecated(message="", version=None): + # type: (str, Optional[str]) -> Callable[[Callable[..., Any]], Callable[..., Any]] + """Decorator function to report a ``DeprecationWarning``. Bear + in mind that `DeprecationWarning` are ignored by default so they're + not available in user logs. To show them, the application must be launched + with a special flag: + + $ python -Wall script.py + + This approach is used by most of the frameworks, including Django + (ref: https://docs.djangoproject.com/en/2.0/howto/upgrade-version/#resolving-deprecation-warnings) + """ + + def decorator(func): + # type: (Callable[..., Any]) -> Callable[..., Any] + @wraps(func) + def wrapper(*args, **kwargs): + # type: (Tuple[Any], Dict[str, Any]) -> Any + msg = format_message(func.__name__, message, version) + warn(msg, stacklevel=3) + return func(*args, **kwargs) + + return wrapper + + return decorator + + +def get_service_legacy(default=None): + # type: (Optional[str]) -> Optional[str] + """Helper to get the old {DD,DATADOG}_SERVICE_NAME environment variables + and output a deprecation warning if they are defined. + + Note that this helper should only be used for migrating integrations which + use the {DD,DATADOG}_SERVICE_NAME variables to the new DD_SERVICE variable. + + If the environment variables are not in use, no deprecation warning is + produced and `default` is returned. + """ + for old_env_key in ["DD_SERVICE_NAME", "DATADOG_SERVICE_NAME"]: + if old_env_key in os.environ: + debtcollector.deprecate( + ( + "'{}' is deprecated and will be removed in a future version. Please use DD_SERVICE instead. " + "Refer to our release notes on Github: https://github.com/DataDog/dd-trace-py/releases/tag/v0.36.0 " + "for the improvements being made for service names." + ).format(old_env_key) + ) + return os.getenv(old_env_key) + + return default diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/formats.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/formats.py new file mode 100644 index 000000000..00ff08cbf --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/formats.py @@ -0,0 +1,154 @@ +import logging +import os +import re +from typing import Any +from typing import Dict +from typing import Optional +from typing import TypeVar +from typing import Union + +from .deprecation import deprecation + + +T = TypeVar("T") + +# Tags `key:value` must be separated by either comma or space +_TAGS_NOT_SEPARATED = re.compile(r":[^,\s]+:") + +log = logging.getLogger(__name__) + + +def get_env(*parts, **kwargs): + # type: (str, T) -> Union[str, T, None] + """Retrieves environment variables value for the given integration. It must be used + for consistency between integrations. The implementation is backward compatible + with legacy nomenclature: + + * `DATADOG_` is a legacy prefix with lower priority + * `DD_` environment variables have the highest priority + * the environment variable is built concatenating `integration` and `variable` + arguments + * return `default` otherwise + + :param parts: environment variable parts that will be joined with ``_`` to generate the name + :type parts: :obj:`str` + :param kwargs: ``default`` is the only supported keyword argument which sets the default value + if no environment variable is found + :rtype: :obj:`str` | ``kwargs["default"]`` + :returns: The string environment variable value or the value of ``kwargs["default"]`` if not found + """ + default = kwargs.get("default") + + key = "_".join(parts) + key = key.upper() + legacy_env = "DATADOG_{}".format(key) + env = "DD_{}".format(key) + + value = os.getenv(env) + legacy = os.getenv(legacy_env) + if legacy: + # Deprecation: `DATADOG_` variables are deprecated + deprecation( + name="DATADOG_", + message="Use `DD_` prefix instead", + version="1.0.0", + ) + + value = value or legacy + return value if value else default + + +def deep_getattr(obj, attr_string, default=None): + # type: (Any, str, Optional[Any]) -> Optional[Any] + """ + Returns the attribute of `obj` at the dotted path given by `attr_string` + If no such attribute is reachable, returns `default` + + >>> deep_getattr(cass, 'cluster') + >> deep_getattr(cass, 'cluster.metadata.partitioner') + u'org.apache.cassandra.dht.Murmur3Partitioner' + + >>> deep_getattr(cass, 'i.dont.exist', default='default') + 'default' + """ + attrs = attr_string.split(".") + for attr in attrs: + try: + obj = getattr(obj, attr) + except AttributeError: + return default + + return obj + + +def asbool(value): + # type: (Union[str, bool, None]) -> bool + """Convert the given String to a boolean object. + + Accepted values are `True` and `1`. + """ + if value is None: + return False + + if isinstance(value, bool): + return value + + return value.lower() in ("true", "1") + + +def parse_tags_str(tags_str): + # type: (Optional[str]) -> Dict[str, str] + """Parse a string of tags typically provided via environment variables. + + The expected string is of the form:: + "key1:value1,key2:value2" + "key1:value1 key2:value2" + + :param tags_str: A string of the above form to parse tags from. + :return: A dict containing the tags that were parsed. + """ + parsed_tags = {} # type: Dict[str, str] + if not tags_str: + return parsed_tags + + if _TAGS_NOT_SEPARATED.search(tags_str): + log.error("Malformed tag string with tags not separated by comma or space '%s'.", tags_str) + return parsed_tags + + # Identify separator based on which successfully identifies the correct + # number of valid tags + numtagseps = tags_str.count(":") + for sep in [",", " "]: + if sum(":" in _ for _ in tags_str.split(sep)) == numtagseps: + break + else: + log.error( + ( + "Failed to find separator for tag string: '%s'.\n" + "Tag strings must be comma or space separated:\n" + " key1:value1,key2:value2\n" + " key1:value1 key2:value2" + ), + tags_str, + ) + return parsed_tags + + for tag in tags_str.split(sep): + try: + key, value = tag.split(":", 1) + + # Validate the tag + if key == "" or value == "" or value.endswith(":"): + raise ValueError + except ValueError: + log.error( + "Malformed tag in tag pair '%s' from tag string '%s'.", + tag, + tags_str, + ) + else: + parsed_tags[key] = value + + return parsed_tags diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/http.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/http.py new file mode 100644 index 000000000..619f31d1c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/http.py @@ -0,0 +1,27 @@ +from typing import Optional + + +def normalize_header_name(header_name): + # type: (Optional[str]) -> Optional[str] + """ + Normalizes an header name to lower case, stripping all its leading and trailing white spaces. + :param header_name: the header name to normalize + :type header_name: str + :return: the normalized header name + :rtype: str + """ + return header_name.strip().lower() if header_name is not None else None + + +def strip_query_string(url): + # type: (str) -> str + """ + Strips the query string from a URL for use as tag in spans. + :param url: The URL to be stripped + :return: The given URL without query strings + """ + hqs, fs, f = url.partition("#") + h, _, _ = hqs.partition("?") + if not f: + return h + return h + fs + f diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/importlib.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/importlib.py new file mode 100644 index 000000000..50ca9b10f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/importlib.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import + +from importlib import import_module +from types import TracebackType +from typing import Any +from typing import Callable +from typing import List +from typing import Optional +from typing import Type + + +class require_modules(object): + """Context manager to check the availability of required modules.""" + + def __init__(self, modules): + # type: (List[str]) -> None + self._missing_modules = [] + for module in modules: + try: + import_module(module) + except ImportError: + self._missing_modules.append(module) + + def __enter__(self): + # type: () -> List[str] + return self._missing_modules + + def __exit__(self, exc_type, exc_value, traceback): + # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None + return + + +def func_name(f): + # type: (Callable[..., Any]) -> str + """Return a human readable version of the function's name.""" + if hasattr(f, "__module__"): + return "%s.%s" % (f.__module__, getattr(f, "__name__", f.__class__.__name__)) + return getattr(f, "__name__", f.__class__.__name__) + + +def module_name(instance): + # type: (Any) -> str + """Return the instance module name.""" + return instance.__class__.__module__.split(".")[0] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/time.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/time.py new file mode 100644 index 000000000..d5577e526 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/time.py @@ -0,0 +1,67 @@ +from types import TracebackType +from typing import Optional +from typing import Type + +from ..internal import compat + + +class StopWatch(object): + """A simple timer/stopwatch helper class. + + Not thread-safe (when a single watch is mutated by multiple threads at + the same time). Thread-safe when used by a single thread (not shared) or + when operations are performed in a thread-safe manner on these objects by + wrapping those operations with locks. + + It will use the `monotonic`_ pypi library to find an appropriate + monotonically increasing time providing function (which typically varies + depending on operating system and Python version). + + .. _monotonic: https://pypi.python.org/pypi/monotonic/ + """ + + def __init__(self): + # type: () -> None + self._started_at = None # type: Optional[float] + self._stopped_at = None # type: Optional[float] + + def start(self): + # type: () -> StopWatch + """Starts the watch.""" + self._started_at = compat.monotonic() + return self + + def elapsed(self): + # type: () -> float + """Get how many seconds have elapsed. + + :return: Number of seconds elapsed + :rtype: float + """ + # NOTE: datetime.timedelta does not support nanoseconds, so keep a float here + if self._started_at is None: + raise RuntimeError("Can not get the elapsed time of a stopwatch" " if it has not been started/stopped") + if self._stopped_at is None: + now = compat.monotonic() + else: + now = self._stopped_at + return now - self._started_at + + def __enter__(self): + # type: () -> StopWatch + """Starts the watch.""" + self.start() + return self + + def __exit__(self, tp, value, traceback): + # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> None + """Stops the watch.""" + self.stop() + + def stop(self): + # type: () -> StopWatch + """Stops the watch.""" + if self._started_at is None: + raise RuntimeError("Can not stop a stopwatch that has not been" " started") + self._stopped_at = compat.monotonic() + return self diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/version.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/version.py new file mode 100644 index 000000000..b508c7111 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/version.py @@ -0,0 +1,44 @@ +import typing + +import packaging.version + + +def parse_version(version): + # type: (str) -> typing.Tuple[int, int, int] + """Convert a version string to a tuple of (major, minor, micro) + + Examples:: + + 1.2.3 -> (1, 2, 3) + 1.2 -> (1, 2, 0) + 1 -> (1, 0, 0) + 1.0.0-beta1 -> (1, 0, 0) + 2020.6.19 -> (2020, 6, 19) + malformed -> (0, 0, 0) + 10.5.0 extra -> (10, 5, 0) + """ + # If we have any spaces/extra text, grab the first part + # "1.0.0 beta1" -> "1.0.0" + # "1.0.0" -> "1.0.0" + # DEV: Versions with spaces will get converted to LegacyVersion, we do this splitting + # to maximize the chances of getting a Version as a parsing result + if " " in version: + version = version.split()[0] + + # version() will not raise an exception, if the version if malformed instead + # we will end up with a LegacyVersion + parsed = packaging.version.parse(version) + + # LegacyVersion.release will always be `None` + if not parsed.release: + return (0, 0, 0) + + # Version.release was added in 17.1 + # packaging >= 20.0 has `Version.{major,minor,micro}`, use the following + # to support older versions of the library + # https://github.com/pypa/packaging/blob/47d40f640fddb7c97b01315419b6a1421d2dedbb/packaging/version.py#L404-L417 + return ( + parsed.release[0] if len(parsed.release) >= 1 else 0, + parsed.release[1] if len(parsed.release) >= 2 else 0, + parsed.release[2] if len(parsed.release) >= 3 else 0, + ) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/wrappers.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/wrappers.py new file mode 100644 index 000000000..c4dbb6600 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/utils/wrappers.py @@ -0,0 +1,91 @@ +import inspect +from typing import Any +from typing import Callable +from typing import Optional +from typing import TYPE_CHECKING +from typing import TypeVar + +from ddtrace.vendor import wrapt + +from .deprecation import deprecated + + +if TYPE_CHECKING: + import ddtrace + + +F = TypeVar("F", bound=Callable[..., Any]) + + +def iswrapped(obj, attr=None): + # type: (Any, Optional[str]) -> bool + """Returns whether an attribute is wrapped or not.""" + if attr is not None: + obj = getattr(obj, attr, None) + return hasattr(obj, "__wrapped__") and isinstance(obj, wrapt.ObjectProxy) + + +def unwrap(obj, attr): + # type: (Any, str) -> None + f = getattr(obj, attr) + setattr(obj, attr, f.__wrapped__) + + +@deprecated("`wrapt` library is used instead", version="1.0.0") +def safe_patch( + patchable, # type: Any + key, # type: str + patch_func, # type: Callable[[F, str, dict, ddtrace.Tracer], F] + service, # type: str + meta, # type: dict + tracer, # type: ddtrace.Tracer +): + # type: (...) -> None + """takes patch_func (signature: takes the orig_method that is + wrapped in the monkey patch == UNBOUND + service and meta) and + attach the patched result to patchable at patchable.key + + - If this is the module/class we can rely on methods being unbound, and just have to + update the __dict__ + - If this is an instance, we have to unbind the current and rebind our + patched method + - If patchable is an instance and if we've already patched at the module/class level + then patchable[key] contains an already patched command! + + To workaround this, check if patchable or patchable.__class__ are ``_dogtraced`` + If is isn't, nothing to worry about, patch the key as usual + But if it is, search for a '__dd_orig_{key}' method on the class, which is + the original unpatched method we wish to trace. + """ + + def _get_original_method(thing, key): + orig = None + if hasattr(thing, "_dogtraced"): + # Search for original method + orig = getattr(thing, "__dd_orig_{}".format(key), None) + else: + orig = getattr(thing, key) + # Set it for the next time we attempt to patch `thing` + setattr(thing, "__dd_orig_{}".format(key), orig) + + return orig + + if inspect.isclass(patchable) or inspect.ismodule(patchable): + orig = _get_original_method(patchable, key) + if not orig: + # Should never happen + return + elif hasattr(patchable, "__class__"): + orig = _get_original_method(patchable.__class__, key) + if not orig: + # Should never happen + return + else: + return + + dest = patch_func(orig, service, meta, tracer) + + if inspect.isclass(patchable) or inspect.ismodule(patchable): + setattr(patchable, key, dest) + elif hasattr(patchable, "__class__"): + setattr(patchable, key, dest.__get__(patchable, patchable.__class__)) # type: ignore[attr-defined] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/__init__.py new file mode 100644 index 000000000..99afe3b78 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/__init__.py @@ -0,0 +1,97 @@ +""" +ddtrace.vendor +============== +Install vendored dependencies under a different top level package to avoid importing `ddtrace/__init__.py` +whenever a dependency is imported. Doing this allows us to have a little more control over import order. + + +Dependencies +============ + +wrapt +----- + +Website: https://wrapt.readthedocs.io/en/latest/ +Source: https://github.com/GrahamDumpleton/wrapt/ +Version: 1.12.1 +License: BSD 2-Clause "Simplified" License + +Notes: + `setup.py` will attempt to build the `wrapt/_wrappers.c` C module + + +dogstatsd +--------- + +Website: https://datadogpy.readthedocs.io/en/latest/ +Source: https://github.com/DataDog/datadogpy +Version: 8e11af2 (0.39.1) +License: Copyright (c) 2020, Datadog + +Notes: + `dogstatsd/__init__.py` was updated to include a copy of the `datadogpy` license: https://github.com/DataDog/datadogpy/blob/master/LICENSE + Only `datadog.dogstatsd` module was vendored to avoid unnecessary dependencies + `datadog/util/compat.py` was copied to `dogstatsd/compat.py` + `datadog/util/format.py` was copied to `dogstatsd/format.py` + version fixed to 8e11af2 + removed type imports + removed unnecessary compat utils + + +monotonic +--------- + +Website: https://pypi.org/project/monotonic/ +Source: https://github.com/atdt/monotonic +Version: 1.5 +License: Apache License 2.0 + +Notes: + The source `monotonic.py` was added as `monotonic/__init__.py` + + No other changes were made + +debtcollector +------------- + +Website: https://docs.openstack.org/debtcollector/latest/index.html +Source: https://github.com/openstack/debtcollector +Version: 1.22.0 +License: Apache License 2.0 + +Notes: + Removed dependency on `pbr` and manually set `__version__` + + +psutil +------ + +Website: https://github.com/giampaolo/psutil +Source: https://github.com/giampaolo/psutil +Version: 5.6.7 +License: BSD 3 + + +contextvars +------------- + +Source: https://github.com/MagicStack/contextvars +Version: 2.4 +License: Apache License 2.0 + +Notes: + - removal of metaclass usage + - formatting + - use a plain old dict instead of immutables.Map + - removal of `*` syntax +""" + +# Initialize `ddtrace.vendor.datadog.base.log` logger with our custom rate limited logger +# DEV: This helps ensure if there are connection issues we do not spam their logs +# DEV: Overwrite `base.log` instead of `get_logger('datadog.dogstatsd')` so we do +# not conflict with any non-vendored datadog.dogstatsd logger +from ..internal.logger import get_logger +from .dogstatsd import base + + +base.log = get_logger('ddtrace.vendor.dogstatsd') diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..e37521c65 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/contextvars/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/contextvars/__init__.py new file mode 100644 index 000000000..f4c5c62dc --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/contextvars/__init__.py @@ -0,0 +1,160 @@ +import threading + +try: + from collections.abc import Mapping +except ImportError: + from collections import Mapping + + +__all__ = ("ContextVar", "Context", "Token", "copy_context") + + +_NO_DEFAULT = object() + + +class Context(Mapping): + def __init__(self): + self._data = {} + self._prev_context = None + + def run(self, callable, *args, **kwargs): + if self._prev_context is not None: + raise RuntimeError("cannot enter context: {} is already entered".format(self)) + + self._prev_context = _get_context() + try: + _set_context(self) + return callable(*args, **kwargs) + finally: + _set_context(self._prev_context) + self._prev_context = None + + def copy(self): + new = Context() + new._data = self._data.copy() + return new + + def __getitem__(self, var): + if not isinstance(var, ContextVar): + raise TypeError("a ContextVar key was expected, got {!r}".format(var)) + return self._data[var] + + def __contains__(self, var): + if not isinstance(var, ContextVar): + raise TypeError("a ContextVar key was expected, got {!r}".format(var)) + return var in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) + + +class ContextVar(object): + def __init__(self, name, default=_NO_DEFAULT): + if not isinstance(name, str): + raise TypeError("context variable name must be a str") + self._name = name + self._default = default + + @property + def name(self): + return self._name + + def get(self, default=_NO_DEFAULT): + ctx = _get_context() + try: + return ctx[self] + except KeyError: + pass + + if default is not _NO_DEFAULT: + return default + + if self._default is not _NO_DEFAULT: + return self._default + + raise LookupError + + def set(self, value): + ctx = _get_context() + data = ctx._data + try: + old_value = data[self] + except KeyError: + old_value = Token.MISSING + + updated_data = data.copy() + updated_data[self] = value + ctx._data = updated_data + return Token(ctx, self, old_value) + + def reset(self, token): + if token._used: + raise RuntimeError("Token has already been used once") + + if token._var is not self: + raise ValueError("Token was created by a different ContextVar") + + if token._context is not _get_context(): + raise ValueError("Token was created in a different Context") + + ctx = token._context + if token._old_value is Token.MISSING: + del ctx._data[token._var] + else: + ctx._data[token._var] = token._old_value + + token._used = True + + def __repr__(self): + r = "".format(id(self)) + + +class Token(object): + + MISSING = object() + + def __init__(self, context, var, old_value): + self._context = context + self._var = var + self._old_value = old_value + self._used = False + + @property + def var(self): + return self._var + + @property + def old_value(self): + return self._old_value + + def __repr__(self): + r = " None + """ + Initialize a DogStatsd object. + + >>> statsd = DogStatsd() + + :envvar DD_AGENT_HOST: the host of the DogStatsd server. + If set, it overrides default value. + :type DD_AGENT_HOST: string + + :envvar DD_DOGSTATSD_PORT: the port of the DogStatsd server. + If set, it overrides default value. + :type DD_DOGSTATSD_PORT: integer + + :envvar DATADOG_TAGS: Tags to attach to every metric reported by dogstatsd client. + :type DATADOG_TAGS: comma-delimited string + + :envvar DD_ENTITY_ID: Tag to identify the client entity. + :type DD_ENTITY_ID: string + + :envvar DD_ENV: the env of the service running the dogstatsd client. + If set, it is appended to the constant (global) tags of the statsd client. + :type DD_ENV: string + + :envvar DD_SERVICE: the name of the service running the dogstatsd client. + If set, it is appended to the constant (global) tags of the statsd client. + :type DD_SERVICE: string + + :envvar DD_VERSION: the version of the service running the dogstatsd client. + If set, it is appended to the constant (global) tags of the statsd client. + :type DD_VERSION: string + + :envvar DD_DOGSTATSD_DISABLE: Disable any statsd metric collection (default False) + :type DD_DOGSTATSD_DISABLE: boolean + + :param host: the host of the DogStatsd server. + :type host: string + + :param port: the port of the DogStatsd server. + :type port: integer + + :max_buffer_size: Deprecated option, do not use it anymore. + :type max_buffer_type: None + + :param namespace: Namespace to prefix all metric names + :type namespace: string + + :param constant_tags: Tags to attach to all metrics + :type constant_tags: list of strings + + :param use_ms: Report timed values in milliseconds instead of seconds (default False) + :type use_ms: boolean + + :param use_default_route: Dynamically set the DogStatsd host to the default route + (Useful when running the client in a container) (Linux only) + :type use_default_route: boolean + + :param socket_path: Communicate with dogstatsd through a UNIX socket instead of + UDP. If set, disables UDP transmission (Linux only) + :type socket_path: string + + :param default_sample_rate: Sample rate to use by default for all metrics + :type default_sample_rate: float + + :param max_buffer_len: Maximum number of bytes to buffer before sending to the server + if sending metrics in batch. If not specified it will be adjusted to a optimal value + depending on the connection type. + :type max_buffer_len: integer + """ + + self.lock = Lock() + + # Check for deprecated option + if max_buffer_size is not None: + log.warning("The parameter max_buffer_size is now deprecated and is not used anymore") + + # Check host and port env vars + agent_host = os.environ.get('DD_AGENT_HOST') + if agent_host and host == DEFAULT_HOST: + host = agent_host + + dogstatsd_port = os.environ.get('DD_DOGSTATSD_PORT') + if dogstatsd_port and port == DEFAULT_PORT: + try: + port = int(dogstatsd_port) + except ValueError: + log.warning("Port number provided in DD_DOGSTATSD_PORT env var is not an integer: \ + %s, using %s as port number", dogstatsd_port, port) + + # Check enabled + if os.environ.get('DD_DOGSTATSD_DISABLE') not in {'True', 'true', 'yes', '1'}: + self._enabled = True + else: + self._enabled = False + + # Connection + self._max_payload_size = max_buffer_len + if socket_path is not None: + self.socket_path = socket_path # type: Optional[text] + self.host = None + self.port = None + transport = "uds" + if not self._max_payload_size: + self._max_payload_size = UDS_OPTIMAL_PAYLOAD_LENGTH + else: + self.socket_path = None + self.host = self.resolve_host(host, use_default_route) + self.port = int(port) + transport = "udp" + if not self._max_payload_size: + self._max_payload_size = UDP_OPTIMAL_PAYLOAD_LENGTH + + # Socket + self.socket = None + self._send = self._send_to_server + self.encoding = 'utf-8' + + # Options + env_tags = [tag for tag in os.environ.get('DATADOG_TAGS', '').split(',') if tag] + # Inject values of DD_* environment variables as global tags. + for var, tag_name in DD_ENV_TAGS_MAPPING.items(): + value = os.environ.get(var, '') + if value: + env_tags.append('{name}:{value}'.format(name=tag_name, value=value)) + if constant_tags is None: + constant_tags = [] + self.constant_tags = constant_tags + env_tags + if namespace is not None: + namespace = text(namespace) + self.namespace = namespace + self.use_ms = use_ms + self.default_sample_rate = default_sample_rate + + # init telemetry version + self._client_tags = [ + "client:py", + "client_version:8e11af2", + "client_transport:{}".format(transport), + ] + self._reset_telemetry() + self._telemetry_flush_interval = telemetry_min_flush_interval + self._telemetry = not disable_telemetry + + def disable_telemetry(self): + self._telemetry = False + + def enable_telemetry(self): + self._telemetry = True + + def __enter__(self): + self.open_buffer() + return self + + def __exit__(self, type, value, traceback): + self.close_buffer() + + @staticmethod + def resolve_host(host, use_default_route): + """ + Resolve the DogStatsd host. + + Args: + host (string): host + use_default_route (bool): use the system default route as host + (overrides the `host` parameter) + """ + if not use_default_route: + return host + + return get_default_route() + + def get_socket(self): + """ + Return a connected socket. + + Note: connect the socket before assigning it to the class instance to + avoid bad thread race conditions. + """ + with self.lock: + if not self.socket: + if self.socket_path is not None: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + sock.setblocking(0) + sock.connect(self.socket_path) + self.socket = sock + else: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setblocking(0) + sock.connect((self.host, self.port)) + self.socket = sock + + return self.socket + + def open_buffer(self, max_buffer_size=None): + """ + Open a buffer to send a batch of metrics. + + You can also use this as a context manager. + + >>> with DogStatsd() as batch: + >>> batch.gauge('users.online', 123) + >>> batch.gauge('active.connections', 1001) + """ + if max_buffer_size is not None: + log.warning("The parameter max_buffer_size is now deprecated and is not used anymore") + self._current_buffer_total_size = 0 + self.buffer = [] + self._send = self._send_to_buffer + + def close_buffer(self): + """ + Flush the buffer and switch back to single metric packets. + """ + self._send = self._send_to_server + + if self.buffer: + # Only send packets if there are packets to send + self._flush_buffer() + + def gauge( + self, + metric, # type: Text + value, # type: float + tags=None, # type: Optional[List[str]] + sample_rate=None # type: Optional[float] + ): # type(...) -> None + """ + Record the value of a gauge, optionally setting a list of tags and a + sample rate. + + >>> statsd.gauge('users.online', 123) + >>> statsd.gauge('active.connections', 1001, tags=["protocol:http"]) + """ + return self._report(metric, 'g', value, tags, sample_rate) + + def increment( + self, + metric, # type: Text + value=1, # type: float + tags=None, # type: Optional[List[str]] + sample_rate=None # type: Optional[float] + ): # type: (...) -> None + """ + Increment a counter, optionally setting a value, tags and a sample + rate. + + >>> statsd.increment('page.views') + >>> statsd.increment('files.transferred', 124) + """ + self._report(metric, 'c', value, tags, sample_rate) + + def decrement(self, metric, value=1, tags=None, sample_rate=None): + """ + Decrement a counter, optionally setting a value, tags and a sample + rate. + + >>> statsd.decrement('files.remaining') + >>> statsd.decrement('active.connections', 2) + """ + metric_value = -value if value else value + self._report(metric, 'c', metric_value, tags, sample_rate) + + def histogram(self, metric, value, tags=None, sample_rate=None): + """ + Sample a histogram value, optionally setting tags and a sample rate. + + >>> statsd.histogram('uploaded.file.size', 1445) + >>> statsd.histogram('album.photo.count', 26, tags=["gender:female"]) + """ + self._report(metric, 'h', value, tags, sample_rate) + + def distribution(self, metric, value, tags=None, sample_rate=None): + """ + Send a global distribution value, optionally setting tags and a sample rate. + + >>> statsd.distribution('uploaded.file.size', 1445) + >>> statsd.distribution('album.photo.count', 26, tags=["gender:female"]) + """ + self._report(metric, 'd', value, tags, sample_rate) + + def timing(self, metric, value, tags=None, sample_rate=None): + """ + Record a timing, optionally setting tags and a sample rate. + + >>> statsd.timing("query.response.time", 1234) + """ + self._report(metric, 'ms', value, tags, sample_rate) + + def timed(self, metric=None, tags=None, sample_rate=None, use_ms=None): + """ + A decorator or context manager that will measure the distribution of a + function's/context's run time. Optionally specify a list of tags or a + sample rate. If the metric is not defined as a decorator, the module + name and function name will be used. The metric is required as a context + manager. + :: + + @statsd.timed('user.query.time', sample_rate=0.5) + def get_user(user_id): + # Do what you need to ... + pass + + # Is equivalent to ... + with statsd.timed('user.query.time', sample_rate=0.5): + # Do what you need to ... + pass + + # Is equivalent to ... + start = time.time() + try: + get_user(user_id) + finally: + statsd.timing('user.query.time', time.time() - start) + """ + return TimedContextManagerDecorator(self, metric, tags, sample_rate, use_ms) + + def distributed(self, metric=None, tags=None, sample_rate=None, use_ms=None): + """ + A decorator or context manager that will measure the distribution of a + function's/context's run time using custom metric distribution. + Optionally specify a list of tags or a sample rate. If the metric is not + defined as a decorator, the module name and function name will be used. + The metric is required as a context manager. + :: + + @statsd.distributed('user.query.time', sample_rate=0.5) + def get_user(user_id): + # Do what you need to ... + pass + + # Is equivalent to ... + with statsd.distributed('user.query.time', sample_rate=0.5): + # Do what you need to ... + pass + + # Is equivalent to ... + start = time.time() + try: + get_user(user_id) + finally: + statsd.distribution('user.query.time', time.time() - start) + """ + return DistributedContextManagerDecorator(self, metric, tags, sample_rate, use_ms) + + def set(self, metric, value, tags=None, sample_rate=None): + """ + Sample a set value. + + >>> statsd.set('visitors.uniques', 999) + """ + self._report(metric, 's', value, tags, sample_rate) + + def close_socket(self): + """ + Closes connected socket if connected. + """ + if self.socket: + try: + self.socket.close() + except OSError as e: + log.error("Unexpected error: %s", str(e)) + self.socket = None + + def _serialize_metric(self, metric, metric_type, value, tags, sample_rate=1): + # Create/format the metric packet + return "%s%s:%s|%s%s%s" % ( + (self.namespace + ".") if self.namespace else "", + metric, + value, + metric_type, + ("|@" + text(sample_rate)) if sample_rate != 1 else "", + ("|#" + ",".join(normalize_tags(tags))) if tags else "", + ) + + def _report(self, metric, metric_type, value, tags, sample_rate): + """ + Create a metric packet and send it. + + More information about the packets' format: http://docs.datadoghq.com/guides/dogstatsd/ + """ + if value is None: + return + + if self._enabled is not True: + return + + if self._telemetry: + self.metrics_count += 1 + + if sample_rate is None: + sample_rate = self.default_sample_rate + + if sample_rate != 1 and random() > sample_rate: + return + + # Resolve the full tag list + tags = self._add_constant_tags(tags) + payload = self._serialize_metric(metric, metric_type, value, tags, sample_rate) + + # Send it + self._send(payload) + + def _reset_telemetry(self): + self.metrics_count = 0 + self.events_count = 0 + self.service_checks_count = 0 + self.bytes_sent = 0 + self.bytes_dropped = 0 + self.packets_sent = 0 + self.packets_dropped = 0 + self._last_flush_time = time.time() + + def _flush_telemetry(self): + telemetry_tags = ",".join(self._add_constant_tags(self._client_tags)) + return "\n".join(( + "datadog.dogstatsd.client.metrics:%s|c|#%s" % (self.metrics_count, telemetry_tags), + "datadog.dogstatsd.client.events:%s|c|#%s" % (self.events_count, telemetry_tags), + "datadog.dogstatsd.client.service_checks:%s|c|#%s" % (self.service_checks_count, telemetry_tags), + "datadog.dogstatsd.client.bytes_sent:%s|c|#%s" % (self.bytes_sent, telemetry_tags), + "datadog.dogstatsd.client.bytes_dropped:%s|c|#%s" % (self.bytes_dropped, telemetry_tags), + "datadog.dogstatsd.client.packets_sent:%s|c|#%s" % (self.packets_sent, telemetry_tags), + "datadog.dogstatsd.client.packets_dropped:%s|c|#%s" % (self.packets_dropped, telemetry_tags), + )) + + def _is_telemetry_flush_time(self): + return self._telemetry and self._last_flush_time + self._telemetry_flush_interval < time.time() + + def _send_to_server(self, packet): + self._xmit_packet(packet, self._telemetry) + if self._is_telemetry_flush_time(): + telemetry = self._flush_telemetry() + if self._xmit_packet(telemetry, False): + self._reset_telemetry() + self.packets_sent += 1 + self.bytes_sent += len(telemetry) + else: + # Telemetry packet has been dropped, keep telemetry data for the next flush + self._last_flush_time = time.time() + self.bytes_dropped += len(telemetry) + self.packets_dropped += 1 + + def _xmit_packet(self, packet, telemetry): + try: + # If set, use socket directly + (self.socket or self.get_socket()).send(packet.encode(self.encoding)) + if telemetry: + self.packets_sent += 1 + self.bytes_sent += len(packet) + return True + except socket.timeout: + # dogstatsd is overflowing, drop the packets (mimicks the UDP behaviour) + pass + except (socket.herror, socket.gaierror) as se: + log.warning("Error submitting packet: {}, dropping the packet and closing the socket".format(se)) + self.close_socket() + except socket.error as se: + if se.errno == errno.EAGAIN: + log.warning("Socket send would block: {}, dropping the packet".format(se)) + else: + log.warning("Error submitting packet: {}, dropping the packet and closing the socket".format(se)) + self.close_socket() + except Exception as e: + log.error("Unexpected error: %s", str(e)) + if telemetry: + self.bytes_dropped += len(packet) + self.packets_dropped += 1 + return False + + def _send_to_buffer(self, packet): + if self._should_flush(len(packet)): + self._flush_buffer() + self.buffer.append(packet) + # Update the current buffer length, including line break to anticipate the final packet size + self._current_buffer_total_size += (len(packet) + 1) + + def _should_flush(self, length_to_be_added): + if self._current_buffer_total_size + length_to_be_added > self._max_payload_size: + return True + return False + + def _flush_buffer(self): + self._send_to_server("\n".join(self.buffer)) + self._current_buffer_total_size = 0 + self.buffer = [] + + def _escape_event_content(self, string): + return string.replace('\n', '\\n') + + def _escape_service_check_message(self, string): + return string.replace('\n', '\\n').replace('m:', 'm\\:') + + def event(self, title, text, alert_type=None, aggregation_key=None, + source_type_name=None, date_happened=None, priority=None, + tags=None, hostname=None): + """ + Send an event. Attributes are the same as the Event API. + http://docs.datadoghq.com/api/ + + >>> statsd.event('Man down!', 'This server needs assistance.') + >>> statsd.event('The web server restarted', 'The web server is up again', alert_type='success') # NOQA + """ + title = self._escape_event_content(title) + text = self._escape_event_content(text) + + # Append all client level tags to every event + tags = self._add_constant_tags(tags) + + string = u'_e{%d,%d}:%s|%s' % (len(title), len(text), title, text) + if date_happened: + string = '%s|d:%d' % (string, date_happened) + if hostname: + string = '%s|h:%s' % (string, hostname) + if aggregation_key: + string = '%s|k:%s' % (string, aggregation_key) + if priority: + string = '%s|p:%s' % (string, priority) + if source_type_name: + string = '%s|s:%s' % (string, source_type_name) + if alert_type: + string = '%s|t:%s' % (string, alert_type) + if tags: + string = '%s|#%s' % (string, ','.join(tags)) + + if len(string) > 8 * 1024: + raise Exception(u'Event "%s" payload is too big (more than 8KB), ' + 'event discarded' % title) + + if self._telemetry: + self.events_count += 1 + self._send(string) + + def service_check(self, check_name, status, tags=None, timestamp=None, + hostname=None, message=None): + """ + Send a service check run. + + >>> statsd.service_check('my_service.check_name', DogStatsd.WARNING) + """ + message = self._escape_service_check_message(message) if message is not None else '' + + string = u'_sc|{0}|{1}'.format(check_name, status) + + # Append all client level tags to every status check + tags = self._add_constant_tags(tags) + + if timestamp: + string = u'{0}|d:{1}'.format(string, timestamp) + if hostname: + string = u'{0}|h:{1}'.format(string, hostname) + if tags: + string = u'{0}|#{1}'.format(string, ','.join(tags)) + if message: + string = u'{0}|m:{1}'.format(string, message) + + if self._telemetry: + self.service_checks_count += 1 + self._send(string) + + def _add_constant_tags(self, tags): + if self.constant_tags: + if tags: + return tags + self.constant_tags + else: + return self.constant_tags + return tags + + +statsd = DogStatsd() diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/dogstatsd/compat.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/dogstatsd/compat.py new file mode 100644 index 000000000..6962643fd --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/dogstatsd/compat.py @@ -0,0 +1,33 @@ +# Unless explicitly stated otherwise all files in this repository are licensed under the BSD-3-Clause License. +# This product includes software developed at Datadog (https://www.datadoghq.com/). +# Copyright 2015-Present Datadog, Inc +# flake8: noqa +""" +Imports for compatibility with Python 2, Python 3 and Google App Engine. +""" +from functools import wraps +import logging +import socket +import sys + +# Note: using `sys.version_info` instead of the helper functions defined here +# so that mypy detects version-specific code paths. Currently, mypy doesn't +# support try/except imports for version-specific code paths either. +# +# https://mypy.readthedocs.io/en/stable/common_issues.html#python-version-and-system-platform-checks + +# Python 3.x +if sys.version_info[0] >= 3: + text = str + +# Python 2.x +else: + text = unicode + +# Python >= 3.5 +if sys.version_info >= (3, 5): + from asyncio import iscoroutinefunction +# Others +else: + def iscoroutinefunction(*args, **kwargs): + return False diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/dogstatsd/context.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/dogstatsd/context.py new file mode 100644 index 000000000..023a5ce7c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/dogstatsd/context.py @@ -0,0 +1,81 @@ +# Unless explicitly stated otherwise all files in this repository are licensed under the BSD-3-Clause License. +# This product includes software developed at Datadog (https://www.datadoghq.com/). +# Copyright 2015-Present Datadog, Inc +# stdlib +from functools import wraps +from time import time + +# datadog +from .context_async import _get_wrapped_co +from .compat import iscoroutinefunction + + +class TimedContextManagerDecorator(object): + """ + A context manager and a decorator which will report the elapsed time in + the context OR in a function call. + """ + def __init__(self, statsd, metric=None, tags=None, sample_rate=1, use_ms=None): + self.statsd = statsd + self.timing_func = statsd.timing + self.metric = metric + self.tags = tags + self.sample_rate = sample_rate + self.use_ms = use_ms + self.elapsed = None + + def __call__(self, func): + """ + Decorator which returns the elapsed time of the function call. + + Default to the function name if metric was not provided. + """ + if not self.metric: + self.metric = '%s.%s' % (func.__module__, func.__name__) + + # Coroutines + if iscoroutinefunction(func): + return _get_wrapped_co(self, func) + + # Others + @wraps(func) + def wrapped(*args, **kwargs): + start = time() + try: + return func(*args, **kwargs) + finally: + self._send(start) + return wrapped + + def __enter__(self): + if not self.metric: + raise TypeError("Cannot used timed without a metric!") + self._start = time() + return self + + def __exit__(self, type, value, traceback): + # Report the elapsed time of the context manager. + self._send(self._start) + + def _send(self, start): + elapsed = time() - start + use_ms = self.use_ms if self.use_ms is not None else self.statsd.use_ms + elapsed = int(round(1000 * elapsed)) if use_ms else elapsed + self.timing_func(self.metric, elapsed, self.tags, self.sample_rate) + self.elapsed = elapsed + + def start(self): + self.__enter__() + + def stop(self): + self.__exit__(None, None, None) + + +class DistributedContextManagerDecorator(TimedContextManagerDecorator): + """ + A context manager and a decorator which will report the elapsed time in + the context OR in a function call using the custom distribution metric. + """ + def __init__(self, statsd, metric=None, tags=None, sample_rate=1, use_ms=None): + super(DistributedContextManagerDecorator, self).__init__(statsd, metric, tags, sample_rate, use_ms) + self.timing_func = statsd.distribution diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/dogstatsd/context_async.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/dogstatsd/context_async.py new file mode 100644 index 000000000..caa52bbb1 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/dogstatsd/context_async.py @@ -0,0 +1,50 @@ +# Unless explicitly stated otherwise all files in this repository are licensed under the BSD-3-Clause License. +# This product includes software developed at Datadog (https://www.datadoghq.com/). +# Copyright 2015-Present Datadog, Inc +""" +Decorator `timed` for coroutine methods. + +Warning: requires Python 3.5 or higher. +""" +# stdlib +import sys + + +# Wrap the Python 3.5+ function in a docstring to avoid syntax errors when +# running mypy in --py2 mode. Currently there is no way to have mypy skip an +# entire file if it has syntax errors. This solution is very hacky; another +# option is to specify the source files to process in mypy.ini (using glob +# inclusion patterns), and omit this file from the list. +# +# https://stackoverflow.com/a/57023749/3776794 +# https://github.com/python/mypy/issues/6897 +ASYNC_SOURCE = r''' +from functools import wraps +from time import time + + +def _get_wrapped_co(self, func): + """ + `timed` wrapper for coroutine methods. + """ + @wraps(func) + async def wrapped_co(*args, **kwargs): + start = time() + try: + result = await func(*args, **kwargs) + return result + finally: + self._send(start) + return wrapped_co +''' + + +def _get_wrapped_co(self, func): + raise NotImplementedError( + u"Decorator `timed` compatibility with coroutine functions" + u" requires Python 3.5 or higher." + ) + + +if sys.version_info >= (3, 5): + exec(compile(ASYNC_SOURCE, __file__, "exec")) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/dogstatsd/format.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/dogstatsd/format.py new file mode 100644 index 000000000..b325d9690 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/dogstatsd/format.py @@ -0,0 +1,33 @@ +# Unless explicitly stated otherwise all files in this repository are licensed under the BSD-3-Clause License. +# This product includes software developed at Datadog (https://www.datadoghq.com/). +# Copyright 2015-Present Datadog, Inc +# stdlib +import calendar +import datetime +import json +import re + +TAG_INVALID_CHARS_RE = re.compile(r"[^\w\d_\-:/\.]", re.UNICODE) +TAG_INVALID_CHARS_SUBS = "_" + + +def pretty_json(obj): + return json.dumps(obj, sort_keys=True, indent=2) + + +def construct_url(host, api_version, path): + return "{}/api/{}/{}".format(host.strip("/"), api_version.strip("/"), path.strip("/")) + + +def construct_path(api_version, path): + return "{}/{}".format(api_version.strip("/"), path.strip("/")) + + +def force_to_epoch_seconds(epoch_sec_or_dt): + if isinstance(epoch_sec_or_dt, datetime.datetime): + return calendar.timegm(epoch_sec_or_dt.timetuple()) + return epoch_sec_or_dt + + +def normalize_tags(tag_list): + return [re.sub(TAG_INVALID_CHARS_RE, TAG_INVALID_CHARS_SUBS, tag) for tag in tag_list] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/dogstatsd/route.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/dogstatsd/route.py new file mode 100644 index 000000000..500a720ab --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/dogstatsd/route.py @@ -0,0 +1,41 @@ +# Unless explicitly stated otherwise all files in this repository are licensed under the BSD-3-Clause License. +# This product includes software developed at Datadog (https://www.datadoghq.com/). +# Copyright 2015-Present Datadog, Inc +""" +Helper(s), resolve the system's default interface. +""" +# stdlib +import socket +import struct + + +class UnresolvableDefaultRoute(Exception): + """ + Unable to resolve system's default route. + """ + + +def get_default_route(): + """ + Return the system default interface using the proc filesystem. + + Returns: + string: default route + + Raises: + `NotImplementedError`: No proc filesystem is found (non-Linux systems) + `StopIteration`: No default route found + """ + try: + with open('/proc/net/route') as f: + for line in f.readlines(): + fields = line.strip().split() + if fields[1] == '00000000': + return socket.inet_ntoa(struct.pack(' + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +""" +import time + + +__all__ = ('monotonic',) + + +try: + monotonic = time.monotonic +except AttributeError: + import ctypes + import ctypes.util + import os + import sys + import threading + try: + if sys.platform == 'darwin': # OS X, iOS + # See Technical Q&A QA1398 of the Mac Developer Library: + # + libc = ctypes.CDLL('/usr/lib/libc.dylib', use_errno=True) + + class mach_timebase_info_data_t(ctypes.Structure): + """System timebase info. Defined in .""" + _fields_ = (('numer', ctypes.c_uint32), + ('denom', ctypes.c_uint32)) + + mach_absolute_time = libc.mach_absolute_time + mach_absolute_time.restype = ctypes.c_uint64 + + timebase = mach_timebase_info_data_t() + libc.mach_timebase_info(ctypes.byref(timebase)) + ticks_per_second = timebase.numer / timebase.denom * 1.0e9 + + def monotonic(): + """Monotonic clock, cannot go backward.""" + return mach_absolute_time() / ticks_per_second + + elif sys.platform.startswith('win32') or sys.platform.startswith('cygwin'): + if sys.platform.startswith('cygwin'): + # Note: cygwin implements clock_gettime (CLOCK_MONOTONIC = 4) since + # version 1.7.6. Using raw WinAPI for maximum version compatibility. + + # Ugly hack using the wrong calling convention (in 32-bit mode) + # because ctypes has no windll under cygwin (and it also seems that + # the code letting you select stdcall in _ctypes doesn't exist under + # the preprocessor definitions relevant to cygwin). + # This is 'safe' because: + # 1. The ABI of GetTickCount and GetTickCount64 is identical for + # both calling conventions because they both have no parameters. + # 2. libffi masks the problem because after making the call it doesn't + # touch anything through esp and epilogue code restores a correct + # esp from ebp afterwards. + try: + kernel32 = ctypes.cdll.kernel32 + except OSError: # 'No such file or directory' + kernel32 = ctypes.cdll.LoadLibrary('kernel32.dll') + else: + kernel32 = ctypes.windll.kernel32 + + GetTickCount64 = getattr(kernel32, 'GetTickCount64', None) + if GetTickCount64: + # Windows Vista / Windows Server 2008 or newer. + GetTickCount64.restype = ctypes.c_ulonglong + + def monotonic(): + """Monotonic clock, cannot go backward.""" + return GetTickCount64() / 1000.0 + + else: + # Before Windows Vista. + GetTickCount = kernel32.GetTickCount + GetTickCount.restype = ctypes.c_uint32 + + get_tick_count_lock = threading.Lock() + get_tick_count_last_sample = 0 + get_tick_count_wraparounds = 0 + + def monotonic(): + """Monotonic clock, cannot go backward.""" + global get_tick_count_last_sample + global get_tick_count_wraparounds + + with get_tick_count_lock: + current_sample = GetTickCount() + if current_sample < get_tick_count_last_sample: + get_tick_count_wraparounds += 1 + get_tick_count_last_sample = current_sample + + final_milliseconds = get_tick_count_wraparounds << 32 + final_milliseconds += get_tick_count_last_sample + return final_milliseconds / 1000.0 + + else: + try: + clock_gettime = ctypes.CDLL(ctypes.util.find_library('c'), + use_errno=True).clock_gettime + except Exception: + clock_gettime = ctypes.CDLL(ctypes.util.find_library('rt'), + use_errno=True).clock_gettime + + class timespec(ctypes.Structure): + """Time specification, as described in clock_gettime(3).""" + _fields_ = (('tv_sec', ctypes.c_long), + ('tv_nsec', ctypes.c_long)) + + if sys.platform.startswith('linux'): + CLOCK_MONOTONIC = 1 + elif sys.platform.startswith('freebsd'): + CLOCK_MONOTONIC = 4 + elif sys.platform.startswith('sunos5'): + CLOCK_MONOTONIC = 4 + elif 'bsd' in sys.platform: + CLOCK_MONOTONIC = 3 + elif sys.platform.startswith('aix'): + CLOCK_MONOTONIC = ctypes.c_longlong(10) + + def monotonic(): + """Monotonic clock, cannot go backward.""" + ts = timespec() + if clock_gettime(CLOCK_MONOTONIC, ctypes.pointer(ts)): + errno = ctypes.get_errno() + raise OSError(errno, os.strerror(errno)) + return ts.tv_sec + ts.tv_nsec / 1.0e9 + + # Perform a sanity-check. + if monotonic() - monotonic() > 0: + raise ValueError('monotonic() is not monotonic!') + + except Exception as e: + raise RuntimeError('no suitable implementation for this system: ' + repr(e)) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/monotonic/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/monotonic/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..07e0f4e25 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/monotonic/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__init__.py new file mode 100644 index 000000000..b267239e2 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__init__.py @@ -0,0 +1,2516 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""psutil is a cross-platform library for retrieving information on +running processes and system utilization (CPU, memory, disks, network, +sensors) in Python. Supported platforms: + + - Linux + - Windows + - macOS + - FreeBSD + - OpenBSD + - NetBSD + - Sun Solaris + - AIX + +Works with Python versions from 2.6 to 3.4+. +""" + +from __future__ import division + +import collections +import contextlib +import datetime +import functools +import os +import signal +import subprocess +import sys +import threading +import time +try: + import pwd +except ImportError: + pwd = None + +from . import _common +from ._common import deprecated_method +from ._common import memoize +from ._common import memoize_when_activated +from ._common import wrap_numbers as _wrap_numbers +from ._compat import long +from ._compat import PermissionError +from ._compat import ProcessLookupError +from ._compat import PY3 as _PY3 + +from ._common import STATUS_DEAD +from ._common import STATUS_DISK_SLEEP +from ._common import STATUS_IDLE +from ._common import STATUS_LOCKED +from ._common import STATUS_PARKED +from ._common import STATUS_RUNNING +from ._common import STATUS_SLEEPING +from ._common import STATUS_STOPPED +from ._common import STATUS_TRACING_STOP +from ._common import STATUS_WAITING +from ._common import STATUS_WAKING +from ._common import STATUS_ZOMBIE + +from ._common import CONN_CLOSE +from ._common import CONN_CLOSE_WAIT +from ._common import CONN_CLOSING +from ._common import CONN_ESTABLISHED +from ._common import CONN_FIN_WAIT1 +from ._common import CONN_FIN_WAIT2 +from ._common import CONN_LAST_ACK +from ._common import CONN_LISTEN +from ._common import CONN_NONE +from ._common import CONN_SYN_RECV +from ._common import CONN_SYN_SENT +from ._common import CONN_TIME_WAIT +from ._common import NIC_DUPLEX_FULL +from ._common import NIC_DUPLEX_HALF +from ._common import NIC_DUPLEX_UNKNOWN + +from ._common import AIX +from ._common import BSD +from ._common import FREEBSD # NOQA +from ._common import LINUX +from ._common import MACOS +from ._common import NETBSD # NOQA +from ._common import OPENBSD # NOQA +from ._common import OSX # deprecated alias +from ._common import POSIX # NOQA +from ._common import SUNOS +from ._common import WINDOWS + +if LINUX: + # This is public API and it will be retrieved from _pslinux.py + # via sys.modules. + PROCFS_PATH = "/proc" + + from . import _pslinux as _psplatform + + from ._pslinux import IOPRIO_CLASS_BE # NOQA + from ._pslinux import IOPRIO_CLASS_IDLE # NOQA + from ._pslinux import IOPRIO_CLASS_NONE # NOQA + from ._pslinux import IOPRIO_CLASS_RT # NOQA + # Linux >= 2.6.36 + if _psplatform.HAS_PRLIMIT: + from ._psutil_linux import RLIM_INFINITY # NOQA + from ._psutil_linux import RLIMIT_AS # NOQA + from ._psutil_linux import RLIMIT_CORE # NOQA + from ._psutil_linux import RLIMIT_CPU # NOQA + from ._psutil_linux import RLIMIT_DATA # NOQA + from ._psutil_linux import RLIMIT_FSIZE # NOQA + from ._psutil_linux import RLIMIT_LOCKS # NOQA + from ._psutil_linux import RLIMIT_MEMLOCK # NOQA + from ._psutil_linux import RLIMIT_NOFILE # NOQA + from ._psutil_linux import RLIMIT_NPROC # NOQA + from ._psutil_linux import RLIMIT_RSS # NOQA + from ._psutil_linux import RLIMIT_STACK # NOQA + # Kinda ugly but considerably faster than using hasattr() and + # setattr() against the module object (we are at import time: + # speed matters). + from . import _psutil_linux + try: + RLIMIT_MSGQUEUE = _psutil_linux.RLIMIT_MSGQUEUE + except AttributeError: + pass + try: + RLIMIT_NICE = _psutil_linux.RLIMIT_NICE + except AttributeError: + pass + try: + RLIMIT_RTPRIO = _psutil_linux.RLIMIT_RTPRIO + except AttributeError: + pass + try: + RLIMIT_RTTIME = _psutil_linux.RLIMIT_RTTIME + except AttributeError: + pass + try: + RLIMIT_SIGPENDING = _psutil_linux.RLIMIT_SIGPENDING + except AttributeError: + pass + +elif WINDOWS: + from . import _pswindows as _psplatform + from ._psutil_windows import ABOVE_NORMAL_PRIORITY_CLASS # NOQA + from ._psutil_windows import BELOW_NORMAL_PRIORITY_CLASS # NOQA + from ._psutil_windows import HIGH_PRIORITY_CLASS # NOQA + from ._psutil_windows import IDLE_PRIORITY_CLASS # NOQA + from ._psutil_windows import NORMAL_PRIORITY_CLASS # NOQA + from ._psutil_windows import REALTIME_PRIORITY_CLASS # NOQA + from ._pswindows import CONN_DELETE_TCB # NOQA + from ._pswindows import IOPRIO_VERYLOW # NOQA + from ._pswindows import IOPRIO_LOW # NOQA + from ._pswindows import IOPRIO_NORMAL # NOQA + from ._pswindows import IOPRIO_HIGH # NOQA + +elif MACOS: + from . import _psosx as _psplatform + +elif BSD: + from . import _psbsd as _psplatform + +elif SUNOS: + from . import _pssunos as _psplatform + from ._pssunos import CONN_BOUND # NOQA + from ._pssunos import CONN_IDLE # NOQA + + # This is public writable API which is read from _pslinux.py and + # _pssunos.py via sys.modules. + PROCFS_PATH = "/proc" + +elif AIX: + from . import _psaix as _psplatform + + # This is public API and it will be retrieved from _pslinux.py + # via sys.modules. + PROCFS_PATH = "/proc" + +else: # pragma: no cover + raise NotImplementedError('platform %s is not supported' % sys.platform) + + +__all__ = [ + # exceptions + "Error", "NoSuchProcess", "ZombieProcess", "AccessDenied", + "TimeoutExpired", + + # constants + "version_info", "__version__", + + "STATUS_RUNNING", "STATUS_IDLE", "STATUS_SLEEPING", "STATUS_DISK_SLEEP", + "STATUS_STOPPED", "STATUS_TRACING_STOP", "STATUS_ZOMBIE", "STATUS_DEAD", + "STATUS_WAKING", "STATUS_LOCKED", "STATUS_WAITING", "STATUS_LOCKED", + "STATUS_PARKED", + + "CONN_ESTABLISHED", "CONN_SYN_SENT", "CONN_SYN_RECV", "CONN_FIN_WAIT1", + "CONN_FIN_WAIT2", "CONN_TIME_WAIT", "CONN_CLOSE", "CONN_CLOSE_WAIT", + "CONN_LAST_ACK", "CONN_LISTEN", "CONN_CLOSING", "CONN_NONE", + + "AF_LINK", + + "NIC_DUPLEX_FULL", "NIC_DUPLEX_HALF", "NIC_DUPLEX_UNKNOWN", + + "POWER_TIME_UNKNOWN", "POWER_TIME_UNLIMITED", + + "BSD", "FREEBSD", "LINUX", "NETBSD", "OPENBSD", "MACOS", "OSX", "POSIX", + "SUNOS", "WINDOWS", "AIX", + + # classes + "Process", "Popen", + + # functions + "pid_exists", "pids", "process_iter", "wait_procs", # proc + "virtual_memory", "swap_memory", # memory + "cpu_times", "cpu_percent", "cpu_times_percent", "cpu_count", # cpu + "cpu_stats", # "cpu_freq", "getloadavg" + "net_io_counters", "net_connections", "net_if_addrs", # network + "net_if_stats", + "disk_io_counters", "disk_partitions", "disk_usage", # disk + # "sensors_temperatures", "sensors_battery", "sensors_fans" # sensors + "users", "boot_time", # others +] + + +__all__.extend(_psplatform.__extra__all__) +__author__ = "Giampaolo Rodola'" +__version__ = "5.6.7" +version_info = tuple([int(num) for num in __version__.split('.')]) + +_timer = getattr(time, 'monotonic', time.time) +AF_LINK = _psplatform.AF_LINK +POWER_TIME_UNLIMITED = _common.POWER_TIME_UNLIMITED +POWER_TIME_UNKNOWN = _common.POWER_TIME_UNKNOWN +_TOTAL_PHYMEM = None +_LOWEST_PID = None + +# Sanity check in case the user messed up with psutil installation +# or did something weird with sys.path. In this case we might end +# up importing a python module using a C extension module which +# was compiled for a different version of psutil. +# We want to prevent that by failing sooner rather than later. +# See: https://github.com/giampaolo/psutil/issues/564 +if (int(__version__.replace('.', '')) != + getattr(_psplatform.cext, 'version', None)): + msg = "version conflict: %r C extension module was built for another " \ + "version of psutil" % getattr(_psplatform.cext, "__file__") + if hasattr(_psplatform.cext, 'version'): + msg += " (%s instead of %s)" % ( + '.'.join([x for x in str(_psplatform.cext.version)]), __version__) + else: + msg += " (different than %s)" % __version__ + msg += "; you may try to 'pip uninstall psutil', manually remove %s" % ( + getattr(_psplatform.cext, "__file__", + "the existing psutil install directory")) + msg += " or clean the virtual env somehow, then reinstall" + raise ImportError(msg) + + +# ===================================================================== +# --- Exceptions +# ===================================================================== + + +class Error(Exception): + """Base exception class. All other psutil exceptions inherit + from this one. + """ + + def __init__(self, msg=""): + Exception.__init__(self, msg) + self.msg = msg + + def __repr__(self): + ret = "psutil.%s %s" % (self.__class__.__name__, self.msg) + return ret.strip() + + __str__ = __repr__ + + +class NoSuchProcess(Error): + """Exception raised when a process with a certain PID doesn't + or no longer exists. + """ + + def __init__(self, pid, name=None, msg=None): + Error.__init__(self, msg) + self.pid = pid + self.name = name + self.msg = msg + if msg is None: + if name: + details = "(pid=%s, name=%s)" % (self.pid, repr(self.name)) + else: + details = "(pid=%s)" % self.pid + self.msg = "process no longer exists " + details + + +class ZombieProcess(NoSuchProcess): + """Exception raised when querying a zombie process. This is + raised on macOS, BSD and Solaris only, and not always: depending + on the query the OS may be able to succeed anyway. + On Linux all zombie processes are querable (hence this is never + raised). Windows doesn't have zombie processes. + """ + + def __init__(self, pid, name=None, ppid=None, msg=None): + NoSuchProcess.__init__(self, msg) + self.pid = pid + self.ppid = ppid + self.name = name + self.msg = msg + if msg is None: + args = ["pid=%s" % pid] + if name: + args.append("name=%s" % repr(self.name)) + if ppid: + args.append("ppid=%s" % self.ppid) + details = "(%s)" % ", ".join(args) + self.msg = "process still exists but it's a zombie " + details + + +class AccessDenied(Error): + """Exception raised when permission to perform an action is denied.""" + + def __init__(self, pid=None, name=None, msg=None): + Error.__init__(self, msg) + self.pid = pid + self.name = name + self.msg = msg + if msg is None: + if (pid is not None) and (name is not None): + self.msg = "(pid=%s, name=%s)" % (pid, repr(name)) + elif (pid is not None): + self.msg = "(pid=%s)" % self.pid + else: + self.msg = "" + + +class TimeoutExpired(Error): + """Raised on Process.wait(timeout) if timeout expires and process + is still alive. + """ + + def __init__(self, seconds, pid=None, name=None): + Error.__init__(self, "timeout after %s seconds" % seconds) + self.seconds = seconds + self.pid = pid + self.name = name + if (pid is not None) and (name is not None): + self.msg += " (pid=%s, name=%s)" % (pid, repr(name)) + elif (pid is not None): + self.msg += " (pid=%s)" % self.pid + + +# Push exception classes into platform specific module namespace. +_psplatform.NoSuchProcess = NoSuchProcess +_psplatform.ZombieProcess = ZombieProcess +_psplatform.AccessDenied = AccessDenied +_psplatform.TimeoutExpired = TimeoutExpired +if POSIX: + from . import _psposix + _psposix.TimeoutExpired = TimeoutExpired + + +# ===================================================================== +# --- Utils +# ===================================================================== + + +if hasattr(_psplatform, 'ppid_map'): + # Faster version (Windows and Linux). + _ppid_map = _psplatform.ppid_map +else: + def _ppid_map(): + """Return a {pid: ppid, ...} dict for all running processes in + one shot. Used to speed up Process.children(). + """ + ret = {} + for pid in pids(): + try: + ret[pid] = _psplatform.Process(pid).ppid() + except (NoSuchProcess, ZombieProcess): + pass + return ret + + +def _assert_pid_not_reused(fun): + """Decorator which raises NoSuchProcess in case a process is no + longer running or its PID has been reused. + """ + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + if not self.is_running(): + raise NoSuchProcess(self.pid, self._name) + return fun(self, *args, **kwargs) + return wrapper + + +def _pprint_secs(secs): + """Format seconds in a human readable form.""" + now = time.time() + secs_ago = int(now - secs) + if secs_ago < 60 * 60 * 24: + fmt = "%H:%M:%S" + else: + fmt = "%Y-%m-%d %H:%M:%S" + return datetime.datetime.fromtimestamp(secs).strftime(fmt) + + +# ===================================================================== +# --- Process class +# ===================================================================== + + +class Process(object): + """Represents an OS process with the given PID. + If PID is omitted current process PID (os.getpid()) is used. + Raise NoSuchProcess if PID does not exist. + + Note that most of the methods of this class do not make sure + the PID of the process being queried has been reused over time. + That means you might end up retrieving an information referring + to another process in case the original one this instance + refers to is gone in the meantime. + + The only exceptions for which process identity is pre-emptively + checked and guaranteed are: + + - parent() + - children() + - nice() (set) + - ionice() (set) + - rlimit() (set) + - cpu_affinity (set) + - suspend() + - resume() + - send_signal() + - terminate() + - kill() + + To prevent this problem for all other methods you can: + - use is_running() before querying the process + - if you're continuously iterating over a set of Process + instances use process_iter() which pre-emptively checks + process identity for every yielded instance + """ + + def __init__(self, pid=None): + self._init(pid) + + def _init(self, pid, _ignore_nsp=False): + if pid is None: + pid = os.getpid() + else: + if not _PY3 and not isinstance(pid, (int, long)): + raise TypeError('pid must be an integer (got %r)' % pid) + if pid < 0: + raise ValueError('pid must be a positive integer (got %s)' + % pid) + self._pid = pid + self._name = None + self._exe = None + self._create_time = None + self._gone = False + self._hash = None + self._lock = threading.RLock() + # used for caching on Windows only (on POSIX ppid may change) + self._ppid = None + # platform-specific modules define an _psplatform.Process + # implementation class + self._proc = _psplatform.Process(pid) + self._last_sys_cpu_times = None + self._last_proc_cpu_times = None + # cache creation time for later use in is_running() method + try: + self.create_time() + except AccessDenied: + # We should never get here as AFAIK we're able to get + # process creation time on all platforms even as a + # limited user. + pass + except ZombieProcess: + # Zombies can still be queried by this class (although + # not always) and pids() return them so just go on. + pass + except NoSuchProcess: + if not _ignore_nsp: + msg = 'no process found with pid %s' % pid + raise NoSuchProcess(pid, None, msg) + else: + self._gone = True + # This pair is supposed to indentify a Process instance + # univocally over time (the PID alone is not enough as + # it might refer to a process whose PID has been reused). + # This will be used later in __eq__() and is_running(). + self._ident = (self.pid, self._create_time) + + def __str__(self): + try: + info = collections.OrderedDict() + except AttributeError: + info = {} # Python 2.6 + info["pid"] = self.pid + try: + info["name"] = self.name() + if self._create_time: + info['started'] = _pprint_secs(self._create_time) + except ZombieProcess: + info["status"] = "zombie" + except NoSuchProcess: + info["status"] = "terminated" + except AccessDenied: + pass + return "%s.%s(%s)" % ( + self.__class__.__module__, + self.__class__.__name__, + ", ".join(["%s=%r" % (k, v) for k, v in info.items()])) + + __repr__ = __str__ + + def __eq__(self, other): + # Test for equality with another Process object based + # on PID and creation time. + if not isinstance(other, Process): + return NotImplemented + return self._ident == other._ident + + def __ne__(self, other): + return not self == other + + def __hash__(self): + if self._hash is None: + self._hash = hash(self._ident) + return self._hash + + @property + def pid(self): + """The process PID.""" + return self._pid + + # --- utility methods + + @contextlib.contextmanager + def oneshot(self): + """Utility context manager which considerably speeds up the + retrieval of multiple process information at the same time. + + Internally different process info (e.g. name, ppid, uids, + gids, ...) may be fetched by using the same routine, but + only one information is returned and the others are discarded. + When using this context manager the internal routine is + executed once (in the example below on name()) and the + other info are cached. + + The cache is cleared when exiting the context manager block. + The advice is to use this every time you retrieve more than + one information about the process. If you're lucky, you'll + get a hell of a speedup. + + >>> import psutil + >>> p = psutil.Process() + >>> with p.oneshot(): + ... p.name() # collect multiple info + ... p.cpu_times() # return cached value + ... p.cpu_percent() # return cached value + ... p.create_time() # return cached value + ... + >>> + """ + with self._lock: + if hasattr(self, "_cache"): + # NOOP: this covers the use case where the user enters the + # context twice: + # + # >>> with p.oneshot(): + # ... with p.oneshot(): + # ... + # + # Also, since as_dict() internally uses oneshot() + # I expect that the code below will be a pretty common + # "mistake" that the user will make, so let's guard + # against that: + # + # >>> with p.oneshot(): + # ... p.as_dict() + # ... + yield + else: + try: + # cached in case cpu_percent() is used + self.cpu_times.cache_activate(self) + # cached in case memory_percent() is used + self.memory_info.cache_activate(self) + # cached in case parent() is used + self.ppid.cache_activate(self) + # cached in case username() is used + if POSIX: + self.uids.cache_activate(self) + # specific implementation cache + self._proc.oneshot_enter() + yield + finally: + self.cpu_times.cache_deactivate(self) + self.memory_info.cache_deactivate(self) + self.ppid.cache_deactivate(self) + if POSIX: + self.uids.cache_deactivate(self) + self._proc.oneshot_exit() + + def as_dict(self, attrs=None, ad_value=None): + """Utility method returning process information as a + hashable dictionary. + If *attrs* is specified it must be a list of strings + reflecting available Process class' attribute names + (e.g. ['cpu_times', 'name']) else all public (read + only) attributes are assumed. + *ad_value* is the value which gets assigned in case + AccessDenied or ZombieProcess exception is raised when + retrieving that particular process information. + """ + valid_names = _as_dict_attrnames + if attrs is not None: + if not isinstance(attrs, (list, tuple, set, frozenset)): + raise TypeError("invalid attrs type %s" % type(attrs)) + attrs = set(attrs) + invalid_names = attrs - valid_names + if invalid_names: + raise ValueError("invalid attr name%s %s" % ( + "s" if len(invalid_names) > 1 else "", + ", ".join(map(repr, invalid_names)))) + + retdict = dict() + ls = attrs or valid_names + with self.oneshot(): + for name in ls: + try: + if name == 'pid': + ret = self.pid + else: + meth = getattr(self, name) + ret = meth() + except (AccessDenied, ZombieProcess): + ret = ad_value + except NotImplementedError: + # in case of not implemented functionality (may happen + # on old or exotic systems) we want to crash only if + # the user explicitly asked for that particular attr + if attrs: + raise + continue + retdict[name] = ret + return retdict + + def parent(self): + """Return the parent process as a Process object pre-emptively + checking whether PID has been reused. + If no parent is known return None. + """ + lowest_pid = _LOWEST_PID if _LOWEST_PID is not None else pids()[0] + if self.pid == lowest_pid: + return None + ppid = self.ppid() + if ppid is not None: + ctime = self.create_time() + try: + parent = Process(ppid) + if parent.create_time() <= ctime: + return parent + # ...else ppid has been reused by another process + except NoSuchProcess: + pass + + def parents(self): + """Return the parents of this process as a list of Process + instances. If no parents are known return an empty list. + """ + parents = [] + proc = self.parent() + while proc is not None: + parents.append(proc) + proc = proc.parent() + return parents + + def is_running(self): + """Return whether this process is running. + It also checks if PID has been reused by another process in + which case return False. + """ + if self._gone: + return False + try: + # Checking if PID is alive is not enough as the PID might + # have been reused by another process: we also want to + # verify process identity. + # Process identity / uniqueness over time is guaranteed by + # (PID + creation time) and that is verified in __eq__. + return self == Process(self.pid) + except ZombieProcess: + # We should never get here as it's already handled in + # Process.__init__; here just for extra safety. + return True + except NoSuchProcess: + self._gone = True + return False + + # --- actual API + + @memoize_when_activated + def ppid(self): + """The process parent PID. + On Windows the return value is cached after first call. + """ + # On POSIX we don't want to cache the ppid as it may unexpectedly + # change to 1 (init) in case this process turns into a zombie: + # https://github.com/giampaolo/psutil/issues/321 + # http://stackoverflow.com/questions/356722/ + + # XXX should we check creation time here rather than in + # Process.parent()? + if POSIX: + return self._proc.ppid() + else: # pragma: no cover + self._ppid = self._ppid or self._proc.ppid() + return self._ppid + + def name(self): + """The process name. The return value is cached after first call.""" + # Process name is only cached on Windows as on POSIX it may + # change, see: + # https://github.com/giampaolo/psutil/issues/692 + if WINDOWS and self._name is not None: + return self._name + name = self._proc.name() + if POSIX and len(name) >= 15: + # On UNIX the name gets truncated to the first 15 characters. + # If it matches the first part of the cmdline we return that + # one instead because it's usually more explicative. + # Examples are "gnome-keyring-d" vs. "gnome-keyring-daemon". + try: + cmdline = self.cmdline() + except AccessDenied: + pass + else: + if cmdline: + extended_name = os.path.basename(cmdline[0]) + if extended_name.startswith(name): + name = extended_name + self._name = name + self._proc._name = name + return name + + def exe(self): + """The process executable as an absolute path. + May also be an empty string. + The return value is cached after first call. + """ + def guess_it(fallback): + # try to guess exe from cmdline[0] in absence of a native + # exe representation + cmdline = self.cmdline() + if cmdline and hasattr(os, 'access') and hasattr(os, 'X_OK'): + exe = cmdline[0] # the possible exe + # Attempt to guess only in case of an absolute path. + # It is not safe otherwise as the process might have + # changed cwd. + if (os.path.isabs(exe) and + os.path.isfile(exe) and + os.access(exe, os.X_OK)): + return exe + if isinstance(fallback, AccessDenied): + raise fallback + return fallback + + if self._exe is None: + try: + exe = self._proc.exe() + except AccessDenied as err: + return guess_it(fallback=err) + else: + if not exe: + # underlying implementation can legitimately return an + # empty string; if that's the case we don't want to + # raise AD while guessing from the cmdline + try: + exe = guess_it(fallback=exe) + except AccessDenied: + pass + self._exe = exe + return self._exe + + def cmdline(self): + """The command line this process has been called with.""" + return self._proc.cmdline() + + def status(self): + """The process current status as a STATUS_* constant.""" + try: + return self._proc.status() + except ZombieProcess: + return STATUS_ZOMBIE + + def username(self): + """The name of the user that owns the process. + On UNIX this is calculated by using *real* process uid. + """ + if POSIX: + if pwd is None: + # might happen if python was installed from sources + raise ImportError( + "requires pwd module shipped with standard python") + real_uid = self.uids().real + try: + return pwd.getpwuid(real_uid).pw_name + except KeyError: + # the uid can't be resolved by the system + return str(real_uid) + else: + return self._proc.username() + + def create_time(self): + """The process creation time as a floating point number + expressed in seconds since the epoch, in UTC. + The return value is cached after first call. + """ + if self._create_time is None: + self._create_time = self._proc.create_time() + return self._create_time + + def cwd(self): + """Process current working directory as an absolute path.""" + return self._proc.cwd() + + def nice(self, value=None): + """Get or set process niceness (priority).""" + if value is None: + return self._proc.nice_get() + else: + if not self.is_running(): + raise NoSuchProcess(self.pid, self._name) + self._proc.nice_set(value) + + if POSIX: + + @memoize_when_activated + def uids(self): + """Return process UIDs as a (real, effective, saved) + namedtuple. + """ + return self._proc.uids() + + def gids(self): + """Return process GIDs as a (real, effective, saved) + namedtuple. + """ + return self._proc.gids() + + def terminal(self): + """The terminal associated with this process, if any, + else None. + """ + return self._proc.terminal() + + def num_fds(self): + """Return the number of file descriptors opened by this + process (POSIX only). + """ + return self._proc.num_fds() + + # Linux, BSD, AIX and Windows only + if hasattr(_psplatform.Process, "io_counters"): + + def io_counters(self): + """Return process I/O statistics as a + (read_count, write_count, read_bytes, write_bytes) + namedtuple. + Those are the number of read/write calls performed and the + amount of bytes read and written by the process. + """ + return self._proc.io_counters() + + # Linux and Windows >= Vista only + if hasattr(_psplatform.Process, "ionice_get"): + + def ionice(self, ioclass=None, value=None): + """Get or set process I/O niceness (priority). + + On Linux *ioclass* is one of the IOPRIO_CLASS_* constants. + *value* is a number which goes from 0 to 7. The higher the + value, the lower the I/O priority of the process. + + On Windows only *ioclass* is used and it can be set to 2 + (normal), 1 (low) or 0 (very low). + + Available on Linux and Windows > Vista only. + """ + if ioclass is None: + if value is not None: + raise ValueError("'ioclass' argument must be specified") + return self._proc.ionice_get() + else: + return self._proc.ionice_set(ioclass, value) + + # Linux only + if hasattr(_psplatform.Process, "rlimit"): + + def rlimit(self, resource, limits=None): + """Get or set process resource limits as a (soft, hard) + tuple. + + *resource* is one of the RLIMIT_* constants. + *limits* is supposed to be a (soft, hard) tuple. + + See "man prlimit" for further info. + Available on Linux only. + """ + if limits is None: + return self._proc.rlimit(resource) + else: + return self._proc.rlimit(resource, limits) + + # Windows, Linux and FreeBSD only + if hasattr(_psplatform.Process, "cpu_affinity_get"): + + def cpu_affinity(self, cpus=None): + """Get or set process CPU affinity. + If specified, *cpus* must be a list of CPUs for which you + want to set the affinity (e.g. [0, 1]). + If an empty list is passed, all egible CPUs are assumed + (and set). + (Windows, Linux and BSD only). + """ + if cpus is None: + return list(set(self._proc.cpu_affinity_get())) + else: + if not cpus: + if hasattr(self._proc, "_get_eligible_cpus"): + cpus = self._proc._get_eligible_cpus() + else: + cpus = tuple(range(len(cpu_times(percpu=True)))) + self._proc.cpu_affinity_set(list(set(cpus))) + + # Linux, FreeBSD, SunOS + if hasattr(_psplatform.Process, "cpu_num"): + + def cpu_num(self): + """Return what CPU this process is currently running on. + The returned number should be <= psutil.cpu_count() + and <= len(psutil.cpu_percent(percpu=True)). + It may be used in conjunction with + psutil.cpu_percent(percpu=True) to observe the system + workload distributed across CPUs. + """ + return self._proc.cpu_num() + + # Linux, macOS, Windows, Solaris, AIX + if hasattr(_psplatform.Process, "environ"): + + def environ(self): + """The environment variables of the process as a dict. Note: this + might not reflect changes made after the process started. """ + return self._proc.environ() + + if WINDOWS: + + def num_handles(self): + """Return the number of handles opened by this process + (Windows only). + """ + return self._proc.num_handles() + + def num_ctx_switches(self): + """Return the number of voluntary and involuntary context + switches performed by this process. + """ + return self._proc.num_ctx_switches() + + def num_threads(self): + """Return the number of threads used by this process.""" + return self._proc.num_threads() + + if hasattr(_psplatform.Process, "threads"): + + def threads(self): + """Return threads opened by process as a list of + (id, user_time, system_time) namedtuples representing + thread id and thread CPU times (user/system). + On OpenBSD this method requires root access. + """ + return self._proc.threads() + + @_assert_pid_not_reused + def children(self, recursive=False): + """Return the children of this process as a list of Process + instances, pre-emptively checking whether PID has been reused. + If *recursive* is True return all the parent descendants. + + Example (A == this process): + + A ─┐ + │ + ├─ B (child) ─┐ + │ └─ X (grandchild) ─┐ + │ └─ Y (great grandchild) + ├─ C (child) + └─ D (child) + + >>> import psutil + >>> p = psutil.Process() + >>> p.children() + B, C, D + >>> p.children(recursive=True) + B, X, Y, C, D + + Note that in the example above if process X disappears + process Y won't be listed as the reference to process A + is lost. + """ + ppid_map = _ppid_map() + ret = [] + if not recursive: + for pid, ppid in ppid_map.items(): + if ppid == self.pid: + try: + child = Process(pid) + # if child happens to be older than its parent + # (self) it means child's PID has been reused + if self.create_time() <= child.create_time(): + ret.append(child) + except (NoSuchProcess, ZombieProcess): + pass + else: + # Construct a {pid: [child pids]} dict + reverse_ppid_map = collections.defaultdict(list) + for pid, ppid in ppid_map.items(): + reverse_ppid_map[ppid].append(pid) + # Recursively traverse that dict, starting from self.pid, + # such that we only call Process() on actual children + seen = set() + stack = [self.pid] + while stack: + pid = stack.pop() + if pid in seen: + # Since pids can be reused while the ppid_map is + # constructed, there may be rare instances where + # there's a cycle in the recorded process "tree". + continue + seen.add(pid) + for child_pid in reverse_ppid_map[pid]: + try: + child = Process(child_pid) + # if child happens to be older than its parent + # (self) it means child's PID has been reused + intime = self.create_time() <= child.create_time() + if intime: + ret.append(child) + stack.append(child_pid) + except (NoSuchProcess, ZombieProcess): + pass + return ret + + def cpu_percent(self, interval=None): + """Return a float representing the current process CPU + utilization as a percentage. + + When *interval* is 0.0 or None (default) compares process times + to system CPU times elapsed since last call, returning + immediately (non-blocking). That means that the first time + this is called it will return a meaningful 0.0 value. + + When *interval* is > 0.0 compares process times to system CPU + times elapsed before and after the interval (blocking). + + In this case is recommended for accuracy that this function + be called with at least 0.1 seconds between calls. + + A value > 100.0 can be returned in case of processes running + multiple threads on different CPU cores. + + The returned value is explicitly NOT split evenly between + all available logical CPUs. This means that a busy loop process + running on a system with 2 logical CPUs will be reported as + having 100% CPU utilization instead of 50%. + + Examples: + + >>> import psutil + >>> p = psutil.Process(os.getpid()) + >>> # blocking + >>> p.cpu_percent(interval=1) + 2.0 + >>> # non-blocking (percentage since last call) + >>> p.cpu_percent(interval=None) + 2.9 + >>> + """ + blocking = interval is not None and interval > 0.0 + if interval is not None and interval < 0: + raise ValueError("interval is not positive (got %r)" % interval) + num_cpus = cpu_count() or 1 + + def timer(): + return _timer() * num_cpus + + if blocking: + st1 = timer() + pt1 = self._proc.cpu_times() + time.sleep(interval) + st2 = timer() + pt2 = self._proc.cpu_times() + else: + st1 = self._last_sys_cpu_times + pt1 = self._last_proc_cpu_times + st2 = timer() + pt2 = self._proc.cpu_times() + if st1 is None or pt1 is None: + self._last_sys_cpu_times = st2 + self._last_proc_cpu_times = pt2 + return 0.0 + + delta_proc = (pt2.user - pt1.user) + (pt2.system - pt1.system) + delta_time = st2 - st1 + # reset values for next call in case of interval == None + self._last_sys_cpu_times = st2 + self._last_proc_cpu_times = pt2 + + try: + # This is the utilization split evenly between all CPUs. + # E.g. a busy loop process on a 2-CPU-cores system at this + # point is reported as 50% instead of 100%. + overall_cpus_percent = ((delta_proc / delta_time) * 100) + except ZeroDivisionError: + # interval was too low + return 0.0 + else: + # Note 1: + # in order to emulate "top" we multiply the value for the num + # of CPU cores. This way the busy process will be reported as + # having 100% (or more) usage. + # + # Note 2: + # taskmgr.exe on Windows differs in that it will show 50% + # instead. + # + # Note 3: + # a percentage > 100 is legitimate as it can result from a + # process with multiple threads running on different CPU + # cores (top does the same), see: + # http://stackoverflow.com/questions/1032357 + # https://github.com/giampaolo/psutil/issues/474 + single_cpu_percent = overall_cpus_percent * num_cpus + return round(single_cpu_percent, 1) + + @memoize_when_activated + def cpu_times(self): + """Return a (user, system, children_user, children_system) + namedtuple representing the accumulated process time, in + seconds. + This is similar to os.times() but per-process. + On macOS and Windows children_user and children_system are + always set to 0. + """ + return self._proc.cpu_times() + + @memoize_when_activated + def memory_info(self): + """Return a namedtuple with variable fields depending on the + platform, representing memory information about the process. + + The "portable" fields available on all plaforms are `rss` and `vms`. + + All numbers are expressed in bytes. + """ + return self._proc.memory_info() + + @deprecated_method(replacement="memory_info") + def memory_info_ex(self): + return self.memory_info() + + def memory_full_info(self): + """This method returns the same information as memory_info(), + plus, on some platform (Linux, macOS, Windows), also provides + additional metrics (USS, PSS and swap). + The additional metrics provide a better representation of actual + process memory usage. + + Namely USS is the memory which is unique to a process and which + would be freed if the process was terminated right now. + + It does so by passing through the whole process address. + As such it usually requires higher user privileges than + memory_info() and is considerably slower. + """ + return self._proc.memory_full_info() + + def memory_percent(self, memtype="rss"): + """Compare process memory to total physical system memory and + calculate process memory utilization as a percentage. + *memtype* argument is a string that dictates what type of + process memory you want to compare against (defaults to "rss"). + The list of available strings can be obtained like this: + + >>> psutil.Process().memory_info()._fields + ('rss', 'vms', 'shared', 'text', 'lib', 'data', 'dirty', 'uss', 'pss') + """ + valid_types = list(_psplatform.pfullmem._fields) + if memtype not in valid_types: + raise ValueError("invalid memtype %r; valid types are %r" % ( + memtype, tuple(valid_types))) + fun = self.memory_info if memtype in _psplatform.pmem._fields else \ + self.memory_full_info + metrics = fun() + value = getattr(metrics, memtype) + + # use cached value if available + total_phymem = _TOTAL_PHYMEM or virtual_memory().total + if not total_phymem > 0: + # we should never get here + raise ValueError( + "can't calculate process memory percent because " + "total physical system memory is not positive (%r)" + % total_phymem) + return (value / float(total_phymem)) * 100 + + if hasattr(_psplatform.Process, "memory_maps"): + def memory_maps(self, grouped=True): + """Return process' mapped memory regions as a list of namedtuples + whose fields are variable depending on the platform. + + If *grouped* is True the mapped regions with the same 'path' + are grouped together and the different memory fields are summed. + + If *grouped* is False every mapped region is shown as a single + entity and the namedtuple will also include the mapped region's + address space ('addr') and permission set ('perms'). + """ + it = self._proc.memory_maps() + if grouped: + d = {} + for tupl in it: + path = tupl[2] + nums = tupl[3:] + try: + d[path] = map(lambda x, y: x + y, d[path], nums) + except KeyError: + d[path] = nums + nt = _psplatform.pmmap_grouped + return [nt(path, *d[path]) for path in d] # NOQA + else: + nt = _psplatform.pmmap_ext + return [nt(*x) for x in it] + + def open_files(self): + """Return files opened by process as a list of + (path, fd) namedtuples including the absolute file name + and file descriptor number. + """ + return self._proc.open_files() + + def connections(self, kind='inet'): + """Return socket connections opened by process as a list of + (fd, family, type, laddr, raddr, status) namedtuples. + The *kind* parameter filters for connections that match the + following criteria: + + +------------+----------------------------------------------------+ + | Kind Value | Connections using | + +------------+----------------------------------------------------+ + | inet | IPv4 and IPv6 | + | inet4 | IPv4 | + | inet6 | IPv6 | + | tcp | TCP | + | tcp4 | TCP over IPv4 | + | tcp6 | TCP over IPv6 | + | udp | UDP | + | udp4 | UDP over IPv4 | + | udp6 | UDP over IPv6 | + | unix | UNIX socket (both UDP and TCP protocols) | + | all | the sum of all the possible families and protocols | + +------------+----------------------------------------------------+ + """ + return self._proc.connections(kind) + + # --- signals + + if POSIX: + def _send_signal(self, sig): + assert not self.pid < 0, self.pid + if self.pid == 0: + # see "man 2 kill" + raise ValueError( + "preventing sending signal to process with PID 0 as it " + "would affect every process in the process group of the " + "calling process (os.getpid()) instead of PID 0") + try: + os.kill(self.pid, sig) + except ProcessLookupError: + if OPENBSD and pid_exists(self.pid): + # We do this because os.kill() lies in case of + # zombie processes. + raise ZombieProcess(self.pid, self._name, self._ppid) + else: + self._gone = True + raise NoSuchProcess(self.pid, self._name) + except PermissionError: + raise AccessDenied(self.pid, self._name) + + @_assert_pid_not_reused + def send_signal(self, sig): + """Send a signal *sig* to process pre-emptively checking + whether PID has been reused (see signal module constants) . + On Windows only SIGTERM is valid and is treated as an alias + for kill(). + """ + if POSIX: + self._send_signal(sig) + else: # pragma: no cover + if sig == signal.SIGTERM: + self._proc.kill() + # py >= 2.7 + elif sig in (getattr(signal, "CTRL_C_EVENT", object()), + getattr(signal, "CTRL_BREAK_EVENT", object())): + self._proc.send_signal(sig) + else: + raise ValueError( + "only SIGTERM, CTRL_C_EVENT and CTRL_BREAK_EVENT signals " + "are supported on Windows") + + @_assert_pid_not_reused + def suspend(self): + """Suspend process execution with SIGSTOP pre-emptively checking + whether PID has been reused. + On Windows this has the effect ot suspending all process threads. + """ + if POSIX: + self._send_signal(signal.SIGSTOP) + else: # pragma: no cover + self._proc.suspend() + + @_assert_pid_not_reused + def resume(self): + """Resume process execution with SIGCONT pre-emptively checking + whether PID has been reused. + On Windows this has the effect of resuming all process threads. + """ + if POSIX: + self._send_signal(signal.SIGCONT) + else: # pragma: no cover + self._proc.resume() + + @_assert_pid_not_reused + def terminate(self): + """Terminate the process with SIGTERM pre-emptively checking + whether PID has been reused. + On Windows this is an alias for kill(). + """ + if POSIX: + self._send_signal(signal.SIGTERM) + else: # pragma: no cover + self._proc.kill() + + @_assert_pid_not_reused + def kill(self): + """Kill the current process with SIGKILL pre-emptively checking + whether PID has been reused. + """ + if POSIX: + self._send_signal(signal.SIGKILL) + else: # pragma: no cover + self._proc.kill() + + def wait(self, timeout=None): + """Wait for process to terminate and, if process is a children + of os.getpid(), also return its exit code, else None. + + If the process is already terminated immediately return None + instead of raising NoSuchProcess. + + If *timeout* (in seconds) is specified and process is still + alive raise TimeoutExpired. + + To wait for multiple Process(es) use psutil.wait_procs(). + """ + if timeout is not None and not timeout >= 0: + raise ValueError("timeout must be a positive integer") + return self._proc.wait(timeout) + + +# ===================================================================== +# --- Popen class +# ===================================================================== + + +class Popen(Process): + """A more convenient interface to stdlib subprocess.Popen class. + It starts a sub process and deals with it exactly as when using + subprocess.Popen class but in addition also provides all the + properties and methods of psutil.Process class as a unified + interface: + + >>> import psutil + >>> from subprocess import PIPE + >>> p = psutil.Popen(["python", "-c", "print 'hi'"], stdout=PIPE) + >>> p.name() + 'python' + >>> p.uids() + user(real=1000, effective=1000, saved=1000) + >>> p.username() + 'giampaolo' + >>> p.communicate() + ('hi\n', None) + >>> p.terminate() + >>> p.wait(timeout=2) + 0 + >>> + + For method names common to both classes such as kill(), terminate() + and wait(), psutil.Process implementation takes precedence. + + Unlike subprocess.Popen this class pre-emptively checks whether PID + has been reused on send_signal(), terminate() and kill() so that + you don't accidentally terminate another process, fixing + http://bugs.python.org/issue6973. + + For a complete documentation refer to: + http://docs.python.org/3/library/subprocess.html + """ + + def __init__(self, *args, **kwargs): + # Explicitly avoid to raise NoSuchProcess in case the process + # spawned by subprocess.Popen terminates too quickly, see: + # https://github.com/giampaolo/psutil/issues/193 + self.__subproc = subprocess.Popen(*args, **kwargs) + self._init(self.__subproc.pid, _ignore_nsp=True) + + def __dir__(self): + return sorted(set(dir(Popen) + dir(subprocess.Popen))) + + def __enter__(self): + if hasattr(self.__subproc, '__enter__'): + self.__subproc.__enter__() + return self + + def __exit__(self, *args, **kwargs): + if hasattr(self.__subproc, '__exit__'): + return self.__subproc.__exit__(*args, **kwargs) + else: + if self.stdout: + self.stdout.close() + if self.stderr: + self.stderr.close() + try: + # Flushing a BufferedWriter may raise an error. + if self.stdin: + self.stdin.close() + finally: + # Wait for the process to terminate, to avoid zombies. + self.wait() + + def __getattribute__(self, name): + try: + return object.__getattribute__(self, name) + except AttributeError: + try: + return object.__getattribute__(self.__subproc, name) + except AttributeError: + raise AttributeError("%s instance has no attribute '%s'" + % (self.__class__.__name__, name)) + + def wait(self, timeout=None): + if self.__subproc.returncode is not None: + return self.__subproc.returncode + ret = super(Popen, self).wait(timeout) + self.__subproc.returncode = ret + return ret + + +# The valid attr names which can be processed by Process.as_dict(). +_as_dict_attrnames = set( + [x for x in dir(Process) if not x.startswith('_') and x not in + ['send_signal', 'suspend', 'resume', 'terminate', 'kill', 'wait', + 'is_running', 'as_dict', 'parent', 'parents', 'children', 'rlimit', + 'memory_info_ex', 'oneshot']]) + + +# ===================================================================== +# --- system processes related functions +# ===================================================================== + + +def pids(): + """Return a list of current running PIDs.""" + global _LOWEST_PID + ret = sorted(_psplatform.pids()) + _LOWEST_PID = ret[0] + return ret + + +def pid_exists(pid): + """Return True if given PID exists in the current process list. + This is faster than doing "pid in psutil.pids()" and + should be preferred. + """ + if pid < 0: + return False + elif pid == 0 and POSIX: + # On POSIX we use os.kill() to determine PID existence. + # According to "man 2 kill" PID 0 has a special meaning + # though: it refers to <> and that is not we want + # to do here. + return pid in pids() + else: + return _psplatform.pid_exists(pid) + + +_pmap = {} +_lock = threading.Lock() + + +def process_iter(attrs=None, ad_value=None): + """Return a generator yielding a Process instance for all + running processes. + + Every new Process instance is only created once and then cached + into an internal table which is updated every time this is used. + + Cached Process instances are checked for identity so that you're + safe in case a PID has been reused by another process, in which + case the cached instance is updated. + + The sorting order in which processes are yielded is based on + their PIDs. + + *attrs* and *ad_value* have the same meaning as in + Process.as_dict(). If *attrs* is specified as_dict() is called + and the resulting dict is stored as a 'info' attribute attached + to returned Process instance. + If *attrs* is an empty list it will retrieve all process info + (slow). + """ + def add(pid): + proc = Process(pid) + if attrs is not None: + proc.info = proc.as_dict(attrs=attrs, ad_value=ad_value) + with _lock: + _pmap[proc.pid] = proc + return proc + + def remove(pid): + with _lock: + _pmap.pop(pid, None) + + a = set(pids()) + b = set(_pmap.keys()) + new_pids = a - b + gone_pids = b - a + for pid in gone_pids: + remove(pid) + + with _lock: + ls = sorted(list(_pmap.items()) + + list(dict.fromkeys(new_pids).items())) + + for pid, proc in ls: + try: + if proc is None: # new process + yield add(pid) + else: + # use is_running() to check whether PID has been reused by + # another process in which case yield a new Process instance + if proc.is_running(): + if attrs is not None: + proc.info = proc.as_dict( + attrs=attrs, ad_value=ad_value) + yield proc + else: + yield add(pid) + except NoSuchProcess: + remove(pid) + except AccessDenied: + # Process creation time can't be determined hence there's + # no way to tell whether the pid of the cached process + # has been reused. Just return the cached version. + if proc is None and pid in _pmap: + try: + yield _pmap[pid] + except KeyError: + # If we get here it is likely that 2 threads were + # using process_iter(). + pass + else: + raise + + +def wait_procs(procs, timeout=None, callback=None): + """Convenience function which waits for a list of processes to + terminate. + + Return a (gone, alive) tuple indicating which processes + are gone and which ones are still alive. + + The gone ones will have a new *returncode* attribute indicating + process exit status (may be None). + + *callback* is a function which gets called every time a process + terminates (a Process instance is passed as callback argument). + + Function will return as soon as all processes terminate or when + *timeout* occurs. + Differently from Process.wait() it will not raise TimeoutExpired if + *timeout* occurs. + + Typical use case is: + + - send SIGTERM to a list of processes + - give them some time to terminate + - send SIGKILL to those ones which are still alive + + Example: + + >>> def on_terminate(proc): + ... print("process {} terminated".format(proc)) + ... + >>> for p in procs: + ... p.terminate() + ... + >>> gone, alive = wait_procs(procs, timeout=3, callback=on_terminate) + >>> for p in alive: + ... p.kill() + """ + def check_gone(proc, timeout): + try: + returncode = proc.wait(timeout=timeout) + except TimeoutExpired: + pass + else: + if returncode is not None or not proc.is_running(): + proc.returncode = returncode + gone.add(proc) + if callback is not None: + callback(proc) + + if timeout is not None and not timeout >= 0: + msg = "timeout must be a positive integer, got %s" % timeout + raise ValueError(msg) + gone = set() + alive = set(procs) + if callback is not None and not callable(callback): + raise TypeError("callback %r is not a callable" % callable) + if timeout is not None: + deadline = _timer() + timeout + + while alive: + if timeout is not None and timeout <= 0: + break + for proc in alive: + # Make sure that every complete iteration (all processes) + # will last max 1 sec. + # We do this because we don't want to wait too long on a + # single process: in case it terminates too late other + # processes may disappear in the meantime and their PID + # reused. + max_timeout = 1.0 / len(alive) + if timeout is not None: + timeout = min((deadline - _timer()), max_timeout) + if timeout <= 0: + break + check_gone(proc, timeout) + else: + check_gone(proc, max_timeout) + alive = alive - gone + + if alive: + # Last attempt over processes survived so far. + # timeout == 0 won't make this function wait any further. + for proc in alive: + check_gone(proc, 0) + alive = alive - gone + + return (list(gone), list(alive)) + + +# ===================================================================== +# --- CPU related functions +# ===================================================================== + + +def cpu_count(logical=True): + """Return the number of logical CPUs in the system (same as + os.cpu_count() in Python 3.4). + + If *logical* is False return the number of physical cores only + (e.g. hyper thread CPUs are excluded). + + Return None if undetermined. + + The return value is cached after first call. + If desired cache can be cleared like this: + + >>> psutil.cpu_count.cache_clear() + """ + if logical: + ret = _psplatform.cpu_count_logical() + else: + ret = _psplatform.cpu_count_physical() + if ret is not None and ret < 1: + ret = None + return ret + + +def cpu_times(percpu=False): + """Return system-wide CPU times as a namedtuple. + Every CPU time represents the seconds the CPU has spent in the + given mode. The namedtuple's fields availability varies depending on the + platform: + + - user + - system + - idle + - nice (UNIX) + - iowait (Linux) + - irq (Linux, FreeBSD) + - softirq (Linux) + - steal (Linux >= 2.6.11) + - guest (Linux >= 2.6.24) + - guest_nice (Linux >= 3.2.0) + + When *percpu* is True return a list of namedtuples for each CPU. + First element of the list refers to first CPU, second element + to second CPU and so on. + The order of the list is consistent across calls. + """ + if not percpu: + return _psplatform.cpu_times() + else: + return _psplatform.per_cpu_times() + + +try: + _last_cpu_times = cpu_times() +except Exception: + # Don't want to crash at import time. + _last_cpu_times = None + +try: + _last_per_cpu_times = cpu_times(percpu=True) +except Exception: + # Don't want to crash at import time. + _last_per_cpu_times = None + + +def _cpu_tot_time(times): + """Given a cpu_time() ntuple calculates the total CPU time + (including idle time). + """ + tot = sum(times) + if LINUX: + # On Linux guest times are already accounted in "user" or + # "nice" times, so we subtract them from total. + # Htop does the same. References: + # https://github.com/giampaolo/psutil/pull/940 + # http://unix.stackexchange.com/questions/178045 + # https://github.com/torvalds/linux/blob/ + # 447976ef4fd09b1be88b316d1a81553f1aa7cd07/kernel/sched/ + # cputime.c#L158 + tot -= getattr(times, "guest", 0) # Linux 2.6.24+ + tot -= getattr(times, "guest_nice", 0) # Linux 3.2.0+ + return tot + + +def _cpu_busy_time(times): + """Given a cpu_time() ntuple calculates the busy CPU time. + We do so by subtracting all idle CPU times. + """ + busy = _cpu_tot_time(times) + busy -= times.idle + # Linux: "iowait" is time during which the CPU does not do anything + # (waits for IO to complete). On Linux IO wait is *not* accounted + # in "idle" time so we subtract it. Htop does the same. + # References: + # https://github.com/torvalds/linux/blob/ + # 447976ef4fd09b1be88b316d1a81553f1aa7cd07/kernel/sched/cputime.c#L244 + busy -= getattr(times, "iowait", 0) + return busy + + +def _cpu_times_deltas(t1, t2): + assert t1._fields == t2._fields, (t1, t2) + field_deltas = [] + for field in _psplatform.scputimes._fields: + field_delta = getattr(t2, field) - getattr(t1, field) + # CPU times are always supposed to increase over time + # or at least remain the same and that's because time + # cannot go backwards. + # Surprisingly sometimes this might not be the case (at + # least on Windows and Linux), see: + # https://github.com/giampaolo/psutil/issues/392 + # https://github.com/giampaolo/psutil/issues/645 + # https://github.com/giampaolo/psutil/issues/1210 + # Trim negative deltas to zero to ignore decreasing fields. + # top does the same. Reference: + # https://gitlab.com/procps-ng/procps/blob/v3.3.12/top/top.c#L5063 + field_delta = max(0, field_delta) + field_deltas.append(field_delta) + return _psplatform.scputimes(*field_deltas) + + +def cpu_percent(interval=None, percpu=False): + """Return a float representing the current system-wide CPU + utilization as a percentage. + + When *interval* is > 0.0 compares system CPU times elapsed before + and after the interval (blocking). + + When *interval* is 0.0 or None compares system CPU times elapsed + since last call or module import, returning immediately (non + blocking). That means the first time this is called it will + return a meaningless 0.0 value which you should ignore. + In this case is recommended for accuracy that this function be + called with at least 0.1 seconds between calls. + + When *percpu* is True returns a list of floats representing the + utilization as a percentage for each CPU. + First element of the list refers to first CPU, second element + to second CPU and so on. + The order of the list is consistent across calls. + + Examples: + + >>> # blocking, system-wide + >>> psutil.cpu_percent(interval=1) + 2.0 + >>> + >>> # blocking, per-cpu + >>> psutil.cpu_percent(interval=1, percpu=True) + [2.0, 1.0] + >>> + >>> # non-blocking (percentage since last call) + >>> psutil.cpu_percent(interval=None) + 2.9 + >>> + """ + global _last_cpu_times + global _last_per_cpu_times + blocking = interval is not None and interval > 0.0 + if interval is not None and interval < 0: + raise ValueError("interval is not positive (got %r)" % interval) + + def calculate(t1, t2): + times_delta = _cpu_times_deltas(t1, t2) + + all_delta = _cpu_tot_time(times_delta) + busy_delta = _cpu_busy_time(times_delta) + + try: + busy_perc = (busy_delta / all_delta) * 100 + except ZeroDivisionError: + return 0.0 + else: + return round(busy_perc, 1) + + # system-wide usage + if not percpu: + if blocking: + t1 = cpu_times() + time.sleep(interval) + else: + t1 = _last_cpu_times + if t1 is None: + # Something bad happened at import time. We'll + # get a meaningful result on the next call. See: + # https://github.com/giampaolo/psutil/pull/715 + t1 = cpu_times() + _last_cpu_times = cpu_times() + return calculate(t1, _last_cpu_times) + # per-cpu usage + else: + ret = [] + if blocking: + tot1 = cpu_times(percpu=True) + time.sleep(interval) + else: + tot1 = _last_per_cpu_times + if tot1 is None: + # Something bad happened at import time. We'll + # get a meaningful result on the next call. See: + # https://github.com/giampaolo/psutil/pull/715 + tot1 = cpu_times(percpu=True) + _last_per_cpu_times = cpu_times(percpu=True) + for t1, t2 in zip(tot1, _last_per_cpu_times): + ret.append(calculate(t1, t2)) + return ret + + +# Use separate global vars for cpu_times_percent() so that it's +# independent from cpu_percent() and they can both be used within +# the same program. +_last_cpu_times_2 = _last_cpu_times +_last_per_cpu_times_2 = _last_per_cpu_times + + +def cpu_times_percent(interval=None, percpu=False): + """Same as cpu_percent() but provides utilization percentages + for each specific CPU time as is returned by cpu_times(). + For instance, on Linux we'll get: + + >>> cpu_times_percent() + cpupercent(user=4.8, nice=0.0, system=4.8, idle=90.5, iowait=0.0, + irq=0.0, softirq=0.0, steal=0.0, guest=0.0, guest_nice=0.0) + >>> + + *interval* and *percpu* arguments have the same meaning as in + cpu_percent(). + """ + global _last_cpu_times_2 + global _last_per_cpu_times_2 + blocking = interval is not None and interval > 0.0 + if interval is not None and interval < 0: + raise ValueError("interval is not positive (got %r)" % interval) + + def calculate(t1, t2): + nums = [] + times_delta = _cpu_times_deltas(t1, t2) + all_delta = _cpu_tot_time(times_delta) + # "scale" is the value to multiply each delta with to get percentages. + # We use "max" to avoid division by zero (if all_delta is 0, then all + # fields are 0 so percentages will be 0 too. all_delta cannot be a + # fraction because cpu times are integers) + scale = 100.0 / max(1, all_delta) + for field_delta in times_delta: + field_perc = field_delta * scale + field_perc = round(field_perc, 1) + # make sure we don't return negative values or values over 100% + field_perc = min(max(0.0, field_perc), 100.0) + nums.append(field_perc) + return _psplatform.scputimes(*nums) + + # system-wide usage + if not percpu: + if blocking: + t1 = cpu_times() + time.sleep(interval) + else: + t1 = _last_cpu_times_2 + if t1 is None: + # Something bad happened at import time. We'll + # get a meaningful result on the next call. See: + # https://github.com/giampaolo/psutil/pull/715 + t1 = cpu_times() + _last_cpu_times_2 = cpu_times() + return calculate(t1, _last_cpu_times_2) + # per-cpu usage + else: + ret = [] + if blocking: + tot1 = cpu_times(percpu=True) + time.sleep(interval) + else: + tot1 = _last_per_cpu_times_2 + if tot1 is None: + # Something bad happened at import time. We'll + # get a meaningful result on the next call. See: + # https://github.com/giampaolo/psutil/pull/715 + tot1 = cpu_times(percpu=True) + _last_per_cpu_times_2 = cpu_times(percpu=True) + for t1, t2 in zip(tot1, _last_per_cpu_times_2): + ret.append(calculate(t1, t2)) + return ret + + +def cpu_stats(): + """Return CPU statistics.""" + return _psplatform.cpu_stats() + + +if hasattr(_psplatform, "cpu_freq"): + + def cpu_freq(percpu=False): + """Return CPU frequency as a nameduple including current, + min and max frequency expressed in Mhz. + + If *percpu* is True and the system supports per-cpu frequency + retrieval (Linux only) a list of frequencies is returned for + each CPU. If not a list with one element is returned. + """ + ret = _psplatform.cpu_freq() + if percpu: + return ret + else: + num_cpus = float(len(ret)) + if num_cpus == 0: + return None + elif num_cpus == 1: + return ret[0] + else: + currs, mins, maxs = 0.0, 0.0, 0.0 + set_none = False + for cpu in ret: + currs += cpu.current + # On Linux if /proc/cpuinfo is used min/max are set + # to None. + if LINUX and cpu.min is None: + set_none = True + continue + mins += cpu.min + maxs += cpu.max + + current = currs / num_cpus + + if set_none: + min_ = max_ = None + else: + min_ = mins / num_cpus + max_ = maxs / num_cpus + + return _common.scpufreq(current, min_, max_) + + __all__.append("cpu_freq") + + +if hasattr(os, "getloadavg") or hasattr(_psplatform, "getloadavg"): + # Perform this hasattr check once on import time to either use the + # platform based code or proxy straight from the os module. + if hasattr(os, "getloadavg"): + getloadavg = os.getloadavg + else: + getloadavg = _psplatform.getloadavg + + __all__.append("getloadavg") + + +# ===================================================================== +# --- system memory related functions +# ===================================================================== + + +def virtual_memory(): + """Return statistics about system memory usage as a namedtuple + including the following fields, expressed in bytes: + + - total: + total physical memory available. + + - available: + the memory that can be given instantly to processes without the + system going into swap. + This is calculated by summing different memory values depending + on the platform and it is supposed to be used to monitor actual + memory usage in a cross platform fashion. + + - percent: + the percentage usage calculated as (total - available) / total * 100 + + - used: + memory used, calculated differently depending on the platform and + designed for informational purposes only: + macOS: active + wired + BSD: active + wired + cached + Linux: total - free + + - free: + memory not being used at all (zeroed) that is readily available; + note that this doesn't reflect the actual memory available + (use 'available' instead) + + Platform-specific fields: + + - active (UNIX): + memory currently in use or very recently used, and so it is in RAM. + + - inactive (UNIX): + memory that is marked as not used. + + - buffers (BSD, Linux): + cache for things like file system metadata. + + - cached (BSD, macOS): + cache for various things. + + - wired (macOS, BSD): + memory that is marked to always stay in RAM. It is never moved to disk. + + - shared (BSD): + memory that may be simultaneously accessed by multiple processes. + + The sum of 'used' and 'available' does not necessarily equal total. + On Windows 'available' and 'free' are the same. + """ + global _TOTAL_PHYMEM + ret = _psplatform.virtual_memory() + # cached for later use in Process.memory_percent() + _TOTAL_PHYMEM = ret.total + return ret + + +def swap_memory(): + """Return system swap memory statistics as a namedtuple including + the following fields: + + - total: total swap memory in bytes + - used: used swap memory in bytes + - free: free swap memory in bytes + - percent: the percentage usage + - sin: no. of bytes the system has swapped in from disk (cumulative) + - sout: no. of bytes the system has swapped out from disk (cumulative) + + 'sin' and 'sout' on Windows are meaningless and always set to 0. + """ + return _psplatform.swap_memory() + + +# ===================================================================== +# --- disks/paritions related functions +# ===================================================================== + + +def disk_usage(path): + """Return disk usage statistics about the given *path* as a + namedtuple including total, used and free space expressed in bytes + plus the percentage usage. + """ + return _psplatform.disk_usage(path) + + +def disk_partitions(all=False): + """Return mounted partitions as a list of + (device, mountpoint, fstype, opts) namedtuple. + 'opts' field is a raw string separated by commas indicating mount + options which may vary depending on the platform. + + If *all* parameter is False return physical devices only and ignore + all others. + """ + return _psplatform.disk_partitions(all) + + +def disk_io_counters(perdisk=False, nowrap=True): + """Return system disk I/O statistics as a namedtuple including + the following fields: + + - read_count: number of reads + - write_count: number of writes + - read_bytes: number of bytes read + - write_bytes: number of bytes written + - read_time: time spent reading from disk (in ms) + - write_time: time spent writing to disk (in ms) + + Platform specific: + + - busy_time: (Linux, FreeBSD) time spent doing actual I/Os (in ms) + - read_merged_count (Linux): number of merged reads + - write_merged_count (Linux): number of merged writes + + If *perdisk* is True return the same information for every + physical disk installed on the system as a dictionary + with partition names as the keys and the namedtuple + described above as the values. + + If *nowrap* is True it detects and adjust the numbers which overflow + and wrap (restart from 0) and add "old value" to "new value" so that + the returned numbers will always be increasing or remain the same, + but never decrease. + "disk_io_counters.cache_clear()" can be used to invalidate the + cache. + + On recent Windows versions 'diskperf -y' command may need to be + executed first otherwise this function won't find any disk. + """ + kwargs = dict(perdisk=perdisk) if LINUX else {} + rawdict = _psplatform.disk_io_counters(**kwargs) + if not rawdict: + return {} if perdisk else None + if nowrap: + rawdict = _wrap_numbers(rawdict, 'psutil.disk_io_counters') + nt = getattr(_psplatform, "sdiskio", _common.sdiskio) + if perdisk: + for disk, fields in rawdict.items(): + rawdict[disk] = nt(*fields) + return rawdict + else: + return nt(*[sum(x) for x in zip(*rawdict.values())]) + + +disk_io_counters.cache_clear = functools.partial( + _wrap_numbers.cache_clear, 'psutil.disk_io_counters') +disk_io_counters.cache_clear.__doc__ = "Clears nowrap argument cache" + + +# ===================================================================== +# --- network related functions +# ===================================================================== + + +def net_io_counters(pernic=False, nowrap=True): + """Return network I/O statistics as a namedtuple including + the following fields: + + - bytes_sent: number of bytes sent + - bytes_recv: number of bytes received + - packets_sent: number of packets sent + - packets_recv: number of packets received + - errin: total number of errors while receiving + - errout: total number of errors while sending + - dropin: total number of incoming packets which were dropped + - dropout: total number of outgoing packets which were dropped + (always 0 on macOS and BSD) + + If *pernic* is True return the same information for every + network interface installed on the system as a dictionary + with network interface names as the keys and the namedtuple + described above as the values. + + If *nowrap* is True it detects and adjust the numbers which overflow + and wrap (restart from 0) and add "old value" to "new value" so that + the returned numbers will always be increasing or remain the same, + but never decrease. + "disk_io_counters.cache_clear()" can be used to invalidate the + cache. + """ + rawdict = _psplatform.net_io_counters() + if not rawdict: + return {} if pernic else None + if nowrap: + rawdict = _wrap_numbers(rawdict, 'psutil.net_io_counters') + if pernic: + for nic, fields in rawdict.items(): + rawdict[nic] = _common.snetio(*fields) + return rawdict + else: + return _common.snetio(*[sum(x) for x in zip(*rawdict.values())]) + + +net_io_counters.cache_clear = functools.partial( + _wrap_numbers.cache_clear, 'psutil.net_io_counters') +net_io_counters.cache_clear.__doc__ = "Clears nowrap argument cache" + + +def net_connections(kind='inet'): + """Return system-wide socket connections as a list of + (fd, family, type, laddr, raddr, status, pid) namedtuples. + In case of limited privileges 'fd' and 'pid' may be set to -1 + and None respectively. + The *kind* parameter filters for connections that fit the + following criteria: + + +------------+----------------------------------------------------+ + | Kind Value | Connections using | + +------------+----------------------------------------------------+ + | inet | IPv4 and IPv6 | + | inet4 | IPv4 | + | inet6 | IPv6 | + | tcp | TCP | + | tcp4 | TCP over IPv4 | + | tcp6 | TCP over IPv6 | + | udp | UDP | + | udp4 | UDP over IPv4 | + | udp6 | UDP over IPv6 | + | unix | UNIX socket (both UDP and TCP protocols) | + | all | the sum of all the possible families and protocols | + +------------+----------------------------------------------------+ + + On macOS this function requires root privileges. + """ + return _psplatform.net_connections(kind) + + +def net_if_addrs(): + """Return the addresses associated to each NIC (network interface + card) installed on the system as a dictionary whose keys are the + NIC names and value is a list of namedtuples for each address + assigned to the NIC. Each namedtuple includes 5 fields: + + - family: can be either socket.AF_INET, socket.AF_INET6 or + psutil.AF_LINK, which refers to a MAC address. + - address: is the primary address and it is always set. + - netmask: and 'broadcast' and 'ptp' may be None. + - ptp: stands for "point to point" and references the + destination address on a point to point interface + (typically a VPN). + - broadcast: and *ptp* are mutually exclusive. + + Note: you can have more than one address of the same family + associated with each interface. + """ + has_enums = sys.version_info >= (3, 4) + if has_enums: + import socket + rawlist = _psplatform.net_if_addrs() + rawlist.sort(key=lambda x: x[1]) # sort by family + ret = collections.defaultdict(list) + for name, fam, addr, mask, broadcast, ptp in rawlist: + if has_enums: + try: + fam = socket.AddressFamily(fam) + except ValueError: + if WINDOWS and fam == -1: + fam = _psplatform.AF_LINK + elif (hasattr(_psplatform, "AF_LINK") and + _psplatform.AF_LINK == fam): + # Linux defines AF_LINK as an alias for AF_PACKET. + # We re-set the family here so that repr(family) + # will show AF_LINK rather than AF_PACKET + fam = _psplatform.AF_LINK + if fam == _psplatform.AF_LINK: + # The underlying C function may return an incomplete MAC + # address in which case we fill it with null bytes, see: + # https://github.com/giampaolo/psutil/issues/786 + separator = ":" if POSIX else "-" + while addr.count(separator) < 5: + addr += "%s00" % separator + ret[name].append(_common.snicaddr(fam, addr, mask, broadcast, ptp)) + return dict(ret) + + +def net_if_stats(): + """Return information about each NIC (network interface card) + installed on the system as a dictionary whose keys are the + NIC names and value is a namedtuple with the following fields: + + - isup: whether the interface is up (bool) + - duplex: can be either NIC_DUPLEX_FULL, NIC_DUPLEX_HALF or + NIC_DUPLEX_UNKNOWN + - speed: the NIC speed expressed in mega bits (MB); if it can't + be determined (e.g. 'localhost') it will be set to 0. + - mtu: the maximum transmission unit expressed in bytes. + """ + return _psplatform.net_if_stats() + + +# ===================================================================== +# --- sensors +# ===================================================================== + + +# Linux, macOS +if hasattr(_psplatform, "sensors_temperatures"): + + def sensors_temperatures(fahrenheit=False): + """Return hardware temperatures. Each entry is a namedtuple + representing a certain hardware sensor (it may be a CPU, an + hard disk or something else, depending on the OS and its + configuration). + All temperatures are expressed in celsius unless *fahrenheit* + is set to True. + """ + def convert(n): + if n is not None: + return (float(n) * 9 / 5) + 32 if fahrenheit else n + + ret = collections.defaultdict(list) + rawdict = _psplatform.sensors_temperatures() + + for name, values in rawdict.items(): + while values: + label, current, high, critical = values.pop(0) + current = convert(current) + high = convert(high) + critical = convert(critical) + + if high and not critical: + critical = high + elif critical and not high: + high = critical + + ret[name].append( + _common.shwtemp(label, current, high, critical)) + + return dict(ret) + + __all__.append("sensors_temperatures") + + +# Linux, macOS +if hasattr(_psplatform, "sensors_fans"): + + def sensors_fans(): + """Return fans speed. Each entry is a namedtuple + representing a certain hardware sensor. + All speed are expressed in RPM (rounds per minute). + """ + return _psplatform.sensors_fans() + + __all__.append("sensors_fans") + + +# Linux, Windows, FreeBSD, macOS +if hasattr(_psplatform, "sensors_battery"): + + def sensors_battery(): + """Return battery information. If no battery is installed + returns None. + + - percent: battery power left as a percentage. + - secsleft: a rough approximation of how many seconds are left + before the battery runs out of power. May be + POWER_TIME_UNLIMITED or POWER_TIME_UNLIMITED. + - power_plugged: True if the AC power cable is connected. + """ + return _psplatform.sensors_battery() + + __all__.append("sensors_battery") + + +# ===================================================================== +# --- other system related functions +# ===================================================================== + + +def boot_time(): + """Return the system boot time expressed in seconds since the epoch.""" + # Note: we are not caching this because it is subject to + # system clock updates. + return _psplatform.boot_time() + + +def users(): + """Return users currently connected on the system as a list of + namedtuples including the following fields. + + - user: the name of the user + - terminal: the tty or pseudo-tty associated with the user, if any. + - host: the host name associated with the entry, if any. + - started: the creation time as a floating point number expressed in + seconds since the epoch. + """ + return _psplatform.users() + + +# ===================================================================== +# --- Windows services +# ===================================================================== + + +if WINDOWS: + + def win_service_iter(): + """Return a generator yielding a WindowsService instance for all + Windows services installed. + """ + return _psplatform.win_service_iter() + + def win_service_get(name): + """Get a Windows service by *name*. + Raise NoSuchProcess if no service with such name exists. + """ + return _psplatform.win_service_get(name) + + +# ===================================================================== + + +def test(): # pragma: no cover + from ._common import bytes2human + from ._compat import get_terminal_size + + today_day = datetime.date.today() + templ = "%-10s %5s %5s %7s %7s %5s %6s %6s %6s %s" + attrs = ['pid', 'memory_percent', 'name', 'cmdline', 'cpu_times', + 'create_time', 'memory_info', 'status', 'nice', 'username'] + print(templ % ("USER", "PID", "%MEM", "VSZ", "RSS", "NICE", + "STATUS", "START", "TIME", "CMDLINE")) + for p in process_iter(attrs, ad_value=None): + if p.info['create_time']: + ctime = datetime.datetime.fromtimestamp(p.info['create_time']) + if ctime.date() == today_day: + ctime = ctime.strftime("%H:%M") + else: + ctime = ctime.strftime("%b%d") + else: + ctime = '' + if p.info['cpu_times']: + cputime = time.strftime("%M:%S", + time.localtime(sum(p.info['cpu_times']))) + else: + cputime = '' + + user = p.info['username'] or '' + if not user and POSIX: + try: + user = p.uids()[0] + except Error: + pass + if user and WINDOWS and '\\' in user: + user = user.split('\\')[1] + user = user[:9] + vms = bytes2human(p.info['memory_info'].vms) if \ + p.info['memory_info'] is not None else '' + rss = bytes2human(p.info['memory_info'].rss) if \ + p.info['memory_info'] is not None else '' + memp = round(p.info['memory_percent'], 1) if \ + p.info['memory_percent'] is not None else '' + nice = int(p.info['nice']) if p.info['nice'] else '' + if p.info['cmdline']: + cmdline = ' '.join(p.info['cmdline']) + else: + cmdline = p.info['name'] + status = p.info['status'][:5] if p.info['status'] else '' + + line = templ % ( + user[:10], + p.info['pid'], + memp, + vms, + rss, + nice, + status, + ctime, + cputime, + cmdline) + print(line[:get_terminal_size()[0]]) + + +del memoize, memoize_when_activated, division, deprecated_method +if sys.version_info[0] < 3: + del num, x + +if __name__ == "__main__": + test() diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..038981095 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_common.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_common.cpython-38.pyc new file mode 100644 index 000000000..a654fef7a Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_common.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_compat.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_compat.cpython-38.pyc new file mode 100644 index 000000000..93d8548d7 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_compat.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_psaix.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_psaix.cpython-38.pyc new file mode 100644 index 000000000..db7925820 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_psaix.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_psbsd.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_psbsd.cpython-38.pyc new file mode 100644 index 000000000..3a96d8337 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_psbsd.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_pslinux.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_pslinux.cpython-38.pyc new file mode 100644 index 000000000..ca386a1d2 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_pslinux.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_psosx.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_psosx.cpython-38.pyc new file mode 100644 index 000000000..c166cd7fe Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_psosx.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_psposix.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_psposix.cpython-38.pyc new file mode 100644 index 000000000..09e8d2150 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_psposix.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_pssunos.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_pssunos.cpython-38.pyc new file mode 100644 index 000000000..2d29b1f34 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_pssunos.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_pswindows.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_pswindows.cpython-38.pyc new file mode 100644 index 000000000..36d59d77d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/_pswindows.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/setup.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/setup.cpython-38.pyc new file mode 100644 index 000000000..ae5f3e868 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/__pycache__/setup.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_common.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_common.py new file mode 100644 index 000000000..a0f5e751b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_common.py @@ -0,0 +1,651 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Common objects shared by __init__.py and _ps*.py modules.""" + +# Note: this module is imported by setup.py so it should not import +# psutil or third-party modules. + +from __future__ import division + +import contextlib +import errno +import functools +import os +import socket +import stat +import sys +import threading +import warnings +from collections import defaultdict +from collections import namedtuple +from socket import AF_INET +from socket import SOCK_DGRAM +from socket import SOCK_STREAM +try: + from socket import AF_INET6 +except ImportError: + AF_INET6 = None +try: + from socket import AF_UNIX +except ImportError: + AF_UNIX = None + +if sys.version_info >= (3, 4): + import enum +else: + enum = None + +# can't take it from _common.py as this script is imported by setup.py +PY3 = sys.version_info[0] == 3 + +__all__ = [ + # constants + 'FREEBSD', 'BSD', 'LINUX', 'NETBSD', 'OPENBSD', 'MACOS', 'OSX', 'POSIX', + 'SUNOS', 'WINDOWS', + 'ENCODING', 'ENCODING_ERRS', 'AF_INET6', + # connection constants + 'CONN_CLOSE', 'CONN_CLOSE_WAIT', 'CONN_CLOSING', 'CONN_ESTABLISHED', + 'CONN_FIN_WAIT1', 'CONN_FIN_WAIT2', 'CONN_LAST_ACK', 'CONN_LISTEN', + 'CONN_NONE', 'CONN_SYN_RECV', 'CONN_SYN_SENT', 'CONN_TIME_WAIT', + # net constants + 'NIC_DUPLEX_FULL', 'NIC_DUPLEX_HALF', 'NIC_DUPLEX_UNKNOWN', + # process status constants + 'STATUS_DEAD', 'STATUS_DISK_SLEEP', 'STATUS_IDLE', 'STATUS_LOCKED', + 'STATUS_RUNNING', 'STATUS_SLEEPING', 'STATUS_STOPPED', 'STATUS_SUSPENDED', + 'STATUS_TRACING_STOP', 'STATUS_WAITING', 'STATUS_WAKE_KILL', + 'STATUS_WAKING', 'STATUS_ZOMBIE', 'STATUS_PARKED', + # named tuples + 'pconn', 'pcputimes', 'pctxsw', 'pgids', 'pio', 'pionice', 'popenfile', + 'pthread', 'puids', 'sconn', 'scpustats', 'sdiskio', 'sdiskpart', + 'sdiskusage', 'snetio', 'snicaddr', 'snicstats', 'sswap', 'suser', + # utility functions + 'conn_tmap', 'deprecated_method', 'isfile_strict', 'memoize', + 'parse_environ_block', 'path_exists_strict', 'usage_percent', + 'supports_ipv6', 'sockfam_to_enum', 'socktype_to_enum', "wrap_numbers", + 'bytes2human', 'conn_to_ntuple', +] + + +# =================================================================== +# --- OS constants +# =================================================================== + + +POSIX = os.name == "posix" +WINDOWS = os.name == "nt" +LINUX = sys.platform.startswith("linux") +MACOS = sys.platform.startswith("darwin") +OSX = MACOS # deprecated alias +FREEBSD = sys.platform.startswith("freebsd") +OPENBSD = sys.platform.startswith("openbsd") +NETBSD = sys.platform.startswith("netbsd") +BSD = FREEBSD or OPENBSD or NETBSD +SUNOS = sys.platform.startswith(("sunos", "solaris")) +AIX = sys.platform.startswith("aix") + + +# =================================================================== +# --- API constants +# =================================================================== + + +# Process.status() +STATUS_RUNNING = "running" +STATUS_SLEEPING = "sleeping" +STATUS_DISK_SLEEP = "disk-sleep" +STATUS_STOPPED = "stopped" +STATUS_TRACING_STOP = "tracing-stop" +STATUS_ZOMBIE = "zombie" +STATUS_DEAD = "dead" +STATUS_WAKE_KILL = "wake-kill" +STATUS_WAKING = "waking" +STATUS_IDLE = "idle" # Linux, macOS, FreeBSD +STATUS_LOCKED = "locked" # FreeBSD +STATUS_WAITING = "waiting" # FreeBSD +STATUS_SUSPENDED = "suspended" # NetBSD +STATUS_PARKED = "parked" # Linux + +# Process.connections() and psutil.net_connections() +CONN_ESTABLISHED = "ESTABLISHED" +CONN_SYN_SENT = "SYN_SENT" +CONN_SYN_RECV = "SYN_RECV" +CONN_FIN_WAIT1 = "FIN_WAIT1" +CONN_FIN_WAIT2 = "FIN_WAIT2" +CONN_TIME_WAIT = "TIME_WAIT" +CONN_CLOSE = "CLOSE" +CONN_CLOSE_WAIT = "CLOSE_WAIT" +CONN_LAST_ACK = "LAST_ACK" +CONN_LISTEN = "LISTEN" +CONN_CLOSING = "CLOSING" +CONN_NONE = "NONE" + +# net_if_stats() +if enum is None: + NIC_DUPLEX_FULL = 2 + NIC_DUPLEX_HALF = 1 + NIC_DUPLEX_UNKNOWN = 0 +else: + class NicDuplex(enum.IntEnum): + NIC_DUPLEX_FULL = 2 + NIC_DUPLEX_HALF = 1 + NIC_DUPLEX_UNKNOWN = 0 + + globals().update(NicDuplex.__members__) + +# sensors_battery() +if enum is None: + POWER_TIME_UNKNOWN = -1 + POWER_TIME_UNLIMITED = -2 +else: + class BatteryTime(enum.IntEnum): + POWER_TIME_UNKNOWN = -1 + POWER_TIME_UNLIMITED = -2 + + globals().update(BatteryTime.__members__) + +# --- others + +ENCODING = sys.getfilesystemencoding() +if not PY3: + ENCODING_ERRS = "replace" +else: + try: + ENCODING_ERRS = sys.getfilesystemencodeerrors() # py 3.6 + except AttributeError: + ENCODING_ERRS = "surrogateescape" if POSIX else "replace" + + +# =================================================================== +# --- namedtuples +# =================================================================== + +# --- for system functions + +# psutil.swap_memory() +sswap = namedtuple('sswap', ['total', 'used', 'free', 'percent', 'sin', + 'sout']) +# psutil.disk_usage() +sdiskusage = namedtuple('sdiskusage', ['total', 'used', 'free', 'percent']) +# psutil.disk_io_counters() +sdiskio = namedtuple('sdiskio', ['read_count', 'write_count', + 'read_bytes', 'write_bytes', + 'read_time', 'write_time']) +# psutil.disk_partitions() +sdiskpart = namedtuple('sdiskpart', ['device', 'mountpoint', 'fstype', 'opts']) +# psutil.net_io_counters() +snetio = namedtuple('snetio', ['bytes_sent', 'bytes_recv', + 'packets_sent', 'packets_recv', + 'errin', 'errout', + 'dropin', 'dropout']) +# psutil.users() +suser = namedtuple('suser', ['name', 'terminal', 'host', 'started', 'pid']) +# psutil.net_connections() +sconn = namedtuple('sconn', ['fd', 'family', 'type', 'laddr', 'raddr', + 'status', 'pid']) +# psutil.net_if_addrs() +snicaddr = namedtuple('snicaddr', + ['family', 'address', 'netmask', 'broadcast', 'ptp']) +# psutil.net_if_stats() +snicstats = namedtuple('snicstats', ['isup', 'duplex', 'speed', 'mtu']) +# psutil.cpu_stats() +scpustats = namedtuple( + 'scpustats', ['ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls']) +# psutil.cpu_freq() +scpufreq = namedtuple('scpufreq', ['current', 'min', 'max']) +# psutil.sensors_temperatures() +shwtemp = namedtuple( + 'shwtemp', ['label', 'current', 'high', 'critical']) +# psutil.sensors_battery() +sbattery = namedtuple('sbattery', ['percent', 'secsleft', 'power_plugged']) +# psutil.sensors_fans() +sfan = namedtuple('sfan', ['label', 'current']) + +# --- for Process methods + +# psutil.Process.cpu_times() +pcputimes = namedtuple('pcputimes', + ['user', 'system', 'children_user', 'children_system']) +# psutil.Process.open_files() +popenfile = namedtuple('popenfile', ['path', 'fd']) +# psutil.Process.threads() +pthread = namedtuple('pthread', ['id', 'user_time', 'system_time']) +# psutil.Process.uids() +puids = namedtuple('puids', ['real', 'effective', 'saved']) +# psutil.Process.gids() +pgids = namedtuple('pgids', ['real', 'effective', 'saved']) +# psutil.Process.io_counters() +pio = namedtuple('pio', ['read_count', 'write_count', + 'read_bytes', 'write_bytes']) +# psutil.Process.ionice() +pionice = namedtuple('pionice', ['ioclass', 'value']) +# psutil.Process.ctx_switches() +pctxsw = namedtuple('pctxsw', ['voluntary', 'involuntary']) +# psutil.Process.connections() +pconn = namedtuple('pconn', ['fd', 'family', 'type', 'laddr', 'raddr', + 'status']) + +# psutil.connections() and psutil.Process.connections() +addr = namedtuple('addr', ['ip', 'port']) + + +# =================================================================== +# --- Process.connections() 'kind' parameter mapping +# =================================================================== + + +conn_tmap = { + "all": ([AF_INET, AF_INET6, AF_UNIX], [SOCK_STREAM, SOCK_DGRAM]), + "tcp": ([AF_INET, AF_INET6], [SOCK_STREAM]), + "tcp4": ([AF_INET], [SOCK_STREAM]), + "udp": ([AF_INET, AF_INET6], [SOCK_DGRAM]), + "udp4": ([AF_INET], [SOCK_DGRAM]), + "inet": ([AF_INET, AF_INET6], [SOCK_STREAM, SOCK_DGRAM]), + "inet4": ([AF_INET], [SOCK_STREAM, SOCK_DGRAM]), + "inet6": ([AF_INET6], [SOCK_STREAM, SOCK_DGRAM]), +} + +if AF_INET6 is not None: + conn_tmap.update({ + "tcp6": ([AF_INET6], [SOCK_STREAM]), + "udp6": ([AF_INET6], [SOCK_DGRAM]), + }) + +if AF_UNIX is not None: + conn_tmap.update({ + "unix": ([AF_UNIX], [SOCK_STREAM, SOCK_DGRAM]), + }) + + +# =================================================================== +# --- utils +# =================================================================== + + +def usage_percent(used, total, round_=None): + """Calculate percentage usage of 'used' against 'total'.""" + try: + ret = (float(used) / total) * 100 + except ZeroDivisionError: + return 0.0 + else: + if round_ is not None: + ret = round(ret, round_) + return ret + + +def memoize(fun): + """A simple memoize decorator for functions supporting (hashable) + positional arguments. + It also provides a cache_clear() function for clearing the cache: + + >>> @memoize + ... def foo() + ... return 1 + ... + >>> foo() + 1 + >>> foo.cache_clear() + >>> + """ + @functools.wraps(fun) + def wrapper(*args, **kwargs): + key = (args, frozenset(sorted(kwargs.items()))) + try: + return cache[key] + except KeyError: + ret = cache[key] = fun(*args, **kwargs) + return ret + + def cache_clear(): + """Clear cache.""" + cache.clear() + + cache = {} + wrapper.cache_clear = cache_clear + return wrapper + + +def memoize_when_activated(fun): + """A memoize decorator which is disabled by default. It can be + activated and deactivated on request. + For efficiency reasons it can be used only against class methods + accepting no arguments. + + >>> class Foo: + ... @memoize + ... def foo() + ... print(1) + ... + >>> f = Foo() + >>> # deactivated (default) + >>> foo() + 1 + >>> foo() + 1 + >>> + >>> # activated + >>> foo.cache_activate(self) + >>> foo() + 1 + >>> foo() + >>> foo() + >>> + """ + @functools.wraps(fun) + def wrapper(self): + try: + # case 1: we previously entered oneshot() ctx + ret = self._cache[fun] + except AttributeError: + # case 2: we never entered oneshot() ctx + return fun(self) + except KeyError: + # case 3: we entered oneshot() ctx but there's no cache + # for this entry yet + ret = self._cache[fun] = fun(self) + return ret + + def cache_activate(proc): + """Activate cache. Expects a Process instance. Cache will be + stored as a "_cache" instance attribute.""" + proc._cache = {} + + def cache_deactivate(proc): + """Deactivate and clear cache.""" + try: + del proc._cache + except AttributeError: + pass + + wrapper.cache_activate = cache_activate + wrapper.cache_deactivate = cache_deactivate + return wrapper + + +def isfile_strict(path): + """Same as os.path.isfile() but does not swallow EACCES / EPERM + exceptions, see: + http://mail.python.org/pipermail/python-dev/2012-June/120787.html + """ + try: + st = os.stat(path) + except OSError as err: + if err.errno in (errno.EPERM, errno.EACCES): + raise + return False + else: + return stat.S_ISREG(st.st_mode) + + +def path_exists_strict(path): + """Same as os.path.exists() but does not swallow EACCES / EPERM + exceptions, see: + http://mail.python.org/pipermail/python-dev/2012-June/120787.html + """ + try: + os.stat(path) + except OSError as err: + if err.errno in (errno.EPERM, errno.EACCES): + raise + return False + else: + return True + + +@memoize +def supports_ipv6(): + """Return True if IPv6 is supported on this platform.""" + if not socket.has_ipv6 or AF_INET6 is None: + return False + try: + sock = socket.socket(AF_INET6, socket.SOCK_STREAM) + with contextlib.closing(sock): + sock.bind(("::1", 0)) + return True + except socket.error: + return False + + +def parse_environ_block(data): + """Parse a C environ block of environment variables into a dictionary.""" + # The block is usually raw data from the target process. It might contain + # trailing garbage and lines that do not look like assignments. + ret = {} + pos = 0 + + # localize global variable to speed up access. + WINDOWS_ = WINDOWS + while True: + next_pos = data.find("\0", pos) + # nul byte at the beginning or double nul byte means finish + if next_pos <= pos: + break + # there might not be an equals sign + equal_pos = data.find("=", pos, next_pos) + if equal_pos > pos: + key = data[pos:equal_pos] + value = data[equal_pos + 1:next_pos] + # Windows expects environment variables to be uppercase only + if WINDOWS_: + key = key.upper() + ret[key] = value + pos = next_pos + 1 + + return ret + + +def sockfam_to_enum(num): + """Convert a numeric socket family value to an IntEnum member. + If it's not a known member, return the numeric value itself. + """ + if enum is None: + return num + else: # pragma: no cover + try: + return socket.AddressFamily(num) + except ValueError: + return num + + +def socktype_to_enum(num): + """Convert a numeric socket type value to an IntEnum member. + If it's not a known member, return the numeric value itself. + """ + if enum is None: + return num + else: # pragma: no cover + try: + return socket.SocketKind(num) + except ValueError: + return num + + +def conn_to_ntuple(fd, fam, type_, laddr, raddr, status, status_map, pid=None): + """Convert a raw connection tuple to a proper ntuple.""" + if fam in (socket.AF_INET, AF_INET6): + if laddr: + laddr = addr(*laddr) + if raddr: + raddr = addr(*raddr) + if type_ == socket.SOCK_STREAM and fam in (AF_INET, AF_INET6): + status = status_map.get(status, CONN_NONE) + else: + status = CONN_NONE # ignore whatever C returned to us + fam = sockfam_to_enum(fam) + type_ = socktype_to_enum(type_) + if pid is None: + return pconn(fd, fam, type_, laddr, raddr, status) + else: + return sconn(fd, fam, type_, laddr, raddr, status, pid) + + +def deprecated_method(replacement): + """A decorator which can be used to mark a method as deprecated + 'replcement' is the method name which will be called instead. + """ + def outer(fun): + msg = "%s() is deprecated and will be removed; use %s() instead" % ( + fun.__name__, replacement) + if fun.__doc__ is None: + fun.__doc__ = msg + + @functools.wraps(fun) + def inner(self, *args, **kwargs): + warnings.warn(msg, category=DeprecationWarning, stacklevel=2) + return getattr(self, replacement)(*args, **kwargs) + return inner + return outer + + +class _WrapNumbers: + """Watches numbers so that they don't overflow and wrap + (reset to zero). + """ + + def __init__(self): + self.lock = threading.Lock() + self.cache = {} + self.reminders = {} + self.reminder_keys = {} + + def _add_dict(self, input_dict, name): + assert name not in self.cache + assert name not in self.reminders + assert name not in self.reminder_keys + self.cache[name] = input_dict + self.reminders[name] = defaultdict(int) + self.reminder_keys[name] = defaultdict(set) + + def _remove_dead_reminders(self, input_dict, name): + """In case the number of keys changed between calls (e.g. a + disk disappears) this removes the entry from self.reminders. + """ + old_dict = self.cache[name] + gone_keys = set(old_dict.keys()) - set(input_dict.keys()) + for gone_key in gone_keys: + for remkey in self.reminder_keys[name][gone_key]: + del self.reminders[name][remkey] + del self.reminder_keys[name][gone_key] + + def run(self, input_dict, name): + """Cache dict and sum numbers which overflow and wrap. + Return an updated copy of `input_dict` + """ + if name not in self.cache: + # This was the first call. + self._add_dict(input_dict, name) + return input_dict + + self._remove_dead_reminders(input_dict, name) + + old_dict = self.cache[name] + new_dict = {} + for key in input_dict.keys(): + input_tuple = input_dict[key] + try: + old_tuple = old_dict[key] + except KeyError: + # The input dict has a new key (e.g. a new disk or NIC) + # which didn't exist in the previous call. + new_dict[key] = input_tuple + continue + + bits = [] + for i in range(len(input_tuple)): + input_value = input_tuple[i] + old_value = old_tuple[i] + remkey = (key, i) + if input_value < old_value: + # it wrapped! + self.reminders[name][remkey] += old_value + self.reminder_keys[name][key].add(remkey) + bits.append(input_value + self.reminders[name][remkey]) + + new_dict[key] = tuple(bits) + + self.cache[name] = input_dict + return new_dict + + def cache_clear(self, name=None): + """Clear the internal cache, optionally only for function 'name'.""" + with self.lock: + if name is None: + self.cache.clear() + self.reminders.clear() + self.reminder_keys.clear() + else: + self.cache.pop(name, None) + self.reminders.pop(name, None) + self.reminder_keys.pop(name, None) + + def cache_info(self): + """Return internal cache dicts as a tuple of 3 elements.""" + with self.lock: + return (self.cache, self.reminders, self.reminder_keys) + + +def wrap_numbers(input_dict, name): + """Given an `input_dict` and a function `name`, adjust the numbers + which "wrap" (restart from zero) across different calls by adding + "old value" to "new value" and return an updated dict. + """ + with _wn.lock: + return _wn.run(input_dict, name) + + +_wn = _WrapNumbers() +wrap_numbers.cache_clear = _wn.cache_clear +wrap_numbers.cache_info = _wn.cache_info + + +def open_binary(fname, **kwargs): + return open(fname, "rb", **kwargs) + + +def open_text(fname, **kwargs): + """On Python 3 opens a file in text mode by using fs encoding and + a proper en/decoding errors handler. + On Python 2 this is just an alias for open(name, 'rt'). + """ + if PY3: + # See: + # https://github.com/giampaolo/psutil/issues/675 + # https://github.com/giampaolo/psutil/pull/733 + kwargs.setdefault('encoding', ENCODING) + kwargs.setdefault('errors', ENCODING_ERRS) + return open(fname, "rt", **kwargs) + + +def bytes2human(n, format="%(value).1f%(symbol)s"): + """Used by various scripts. See: + http://goo.gl/zeJZl + + >>> bytes2human(10000) + '9.8K' + >>> bytes2human(100001221) + '95.4M' + """ + symbols = ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') + prefix = {} + for i, s in enumerate(symbols[1:]): + prefix[s] = 1 << (i + 1) * 10 + for symbol in reversed(symbols[1:]): + if n >= prefix[symbol]: + value = float(n) / prefix[symbol] + return format % locals() + return format % dict(symbol=symbols[0], value=n) + + +def get_procfs_path(): + """Return updated psutil.PROCFS_PATH constant.""" + return sys.modules['ddtrace.vendor.psutil'].PROCFS_PATH + + +if PY3: + def decode(s): + return s.decode(encoding=ENCODING, errors=ENCODING_ERRS) +else: + def decode(s): + return s diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_compat.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_compat.py new file mode 100644 index 000000000..07ab909a8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_compat.py @@ -0,0 +1,332 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Module which provides compatibility with older Python versions.""" + +import collections +import errno +import functools +import os +import sys + +__all__ = ["PY3", "long", "xrange", "unicode", "basestring", "u", "b", + "lru_cache", "which", "get_terminal_size", + "FileNotFoundError", "PermissionError", "ProcessLookupError", + "InterruptedError", "ChildProcessError", "FileExistsError"] + +PY3 = sys.version_info[0] == 3 + +if PY3: + long = int + xrange = range + unicode = str + basestring = str + + def u(s): + return s + + def b(s): + return s.encode("latin-1") +else: + long = long + xrange = xrange + unicode = unicode + basestring = basestring + + def u(s): + return unicode(s, "unicode_escape") + + def b(s): + return s + + +# --- exceptions + + +if PY3: + FileNotFoundError = FileNotFoundError # NOQA + PermissionError = PermissionError # NOQA + ProcessLookupError = ProcessLookupError # NOQA + InterruptedError = InterruptedError # NOQA + ChildProcessError = ChildProcessError # NOQA + FileExistsError = FileExistsError # NOQA +else: + # https://github.com/PythonCharmers/python-future/blob/exceptions/ + # src/future/types/exceptions/pep3151.py + + def instance_checking_exception(base_exception=Exception): + def wrapped(instance_checker): + class TemporaryClass(base_exception): + + def __init__(self, *args, **kwargs): + if len(args) == 1 and isinstance(args[0], TemporaryClass): + unwrap_me = args[0] + for attr in dir(unwrap_me): + if not attr.startswith('__'): + setattr(self, attr, getattr(unwrap_me, attr)) + else: + super(TemporaryClass, self).__init__(*args, **kwargs) + + class __metaclass__(type): + def __instancecheck__(cls, inst): + return instance_checker(inst) + + def __subclasscheck__(cls, classinfo): + value = sys.exc_info()[1] + return isinstance(value, cls) + + TemporaryClass.__name__ = instance_checker.__name__ + TemporaryClass.__doc__ = instance_checker.__doc__ + return TemporaryClass + + return wrapped + + @instance_checking_exception(EnvironmentError) + def FileNotFoundError(inst): + return getattr(inst, 'errno', object()) == errno.ENOENT + + @instance_checking_exception(EnvironmentError) + def ProcessLookupError(inst): + return getattr(inst, 'errno', object()) == errno.ESRCH + + @instance_checking_exception(EnvironmentError) + def PermissionError(inst): + return getattr(inst, 'errno', object()) in ( + errno.EACCES, errno.EPERM) + + @instance_checking_exception(EnvironmentError) + def InterruptedError(inst): + return getattr(inst, 'errno', object()) == errno.EINTR + + @instance_checking_exception(EnvironmentError) + def ChildProcessError(inst): + return getattr(inst, 'errno', object()) == errno.ECHILD + + @instance_checking_exception(EnvironmentError) + def FileExistsError(inst): + return getattr(inst, 'errno', object()) == errno.EEXIST + + +# --- stdlib additions + + +# py 3.2 functools.lru_cache +# Taken from: http://code.activestate.com/recipes/578078 +# Credit: Raymond Hettinger +try: + from functools import lru_cache +except ImportError: + try: + from threading import RLock + except ImportError: + from dummy_threading import RLock + + _CacheInfo = collections.namedtuple( + "CacheInfo", ["hits", "misses", "maxsize", "currsize"]) + + class _HashedSeq(list): + __slots__ = 'hashvalue' + + def __init__(self, tup, hash=hash): + self[:] = tup + self.hashvalue = hash(tup) + + def __hash__(self): + return self.hashvalue + + def _make_key(args, kwds, typed, + kwd_mark=(object(), ), + fasttypes=set((int, str, frozenset, type(None))), + sorted=sorted, tuple=tuple, type=type, len=len): + key = args + if kwds: + sorted_items = sorted(kwds.items()) + key += kwd_mark + for item in sorted_items: + key += item + if typed: + key += tuple(type(v) for v in args) + if kwds: + key += tuple(type(v) for k, v in sorted_items) + elif len(key) == 1 and type(key[0]) in fasttypes: + return key[0] + return _HashedSeq(key) + + def lru_cache(maxsize=100, typed=False): + """Least-recently-used cache decorator, see: + http://docs.python.org/3/library/functools.html#functools.lru_cache + """ + def decorating_function(user_function): + cache = dict() + stats = [0, 0] + HITS, MISSES = 0, 1 + make_key = _make_key + cache_get = cache.get + _len = len + lock = RLock() + root = [] + root[:] = [root, root, None, None] + nonlocal_root = [root] + PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 + if maxsize == 0: + def wrapper(*args, **kwds): + result = user_function(*args, **kwds) + stats[MISSES] += 1 + return result + elif maxsize is None: + def wrapper(*args, **kwds): + key = make_key(args, kwds, typed) + result = cache_get(key, root) + if result is not root: + stats[HITS] += 1 + return result + result = user_function(*args, **kwds) + cache[key] = result + stats[MISSES] += 1 + return result + else: + def wrapper(*args, **kwds): + if kwds or typed: + key = make_key(args, kwds, typed) + else: + key = args + lock.acquire() + try: + link = cache_get(key) + if link is not None: + root, = nonlocal_root + link_prev, link_next, key, result = link + link_prev[NEXT] = link_next + link_next[PREV] = link_prev + last = root[PREV] + last[NEXT] = root[PREV] = link + link[PREV] = last + link[NEXT] = root + stats[HITS] += 1 + return result + finally: + lock.release() + result = user_function(*args, **kwds) + lock.acquire() + try: + root, = nonlocal_root + if key in cache: + pass + elif _len(cache) >= maxsize: + oldroot = root + oldroot[KEY] = key + oldroot[RESULT] = result + root = nonlocal_root[0] = oldroot[NEXT] + oldkey = root[KEY] + root[KEY] = root[RESULT] = None + del cache[oldkey] + cache[key] = oldroot + else: + last = root[PREV] + link = [last, root, key, result] + last[NEXT] = root[PREV] = cache[key] = link + stats[MISSES] += 1 + finally: + lock.release() + return result + + def cache_info(): + """Report cache statistics""" + lock.acquire() + try: + return _CacheInfo(stats[HITS], stats[MISSES], maxsize, + len(cache)) + finally: + lock.release() + + def cache_clear(): + """Clear the cache and cache statistics""" + lock.acquire() + try: + cache.clear() + root = nonlocal_root[0] + root[:] = [root, root, None, None] + stats[:] = [0, 0] + finally: + lock.release() + + wrapper.__wrapped__ = user_function + wrapper.cache_info = cache_info + wrapper.cache_clear = cache_clear + return functools.update_wrapper(wrapper, user_function) + + return decorating_function + + +# python 3.3 +try: + from shutil import which +except ImportError: + def which(cmd, mode=os.F_OK | os.X_OK, path=None): + """Given a command, mode, and a PATH string, return the path which + conforms to the given mode on the PATH, or None if there is no such + file. + + `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result + of os.environ.get("PATH"), or can be overridden with a custom search + path. + """ + def _access_check(fn, mode): + return (os.path.exists(fn) and os.access(fn, mode) and + not os.path.isdir(fn)) + + if os.path.dirname(cmd): + if _access_check(cmd, mode): + return cmd + return None + + if path is None: + path = os.environ.get("PATH", os.defpath) + if not path: + return None + path = path.split(os.pathsep) + + if sys.platform == "win32": + if os.curdir not in path: + path.insert(0, os.curdir) + + pathext = os.environ.get("PATHEXT", "").split(os.pathsep) + if any(cmd.lower().endswith(ext.lower()) for ext in pathext): + files = [cmd] + else: + files = [cmd + ext for ext in pathext] + else: + files = [cmd] + + seen = set() + for dir in path: + normdir = os.path.normcase(dir) + if normdir not in seen: + seen.add(normdir) + for thefile in files: + name = os.path.join(dir, thefile) + if _access_check(name, mode): + return name + return None + + +# python 3.3 +try: + from shutil import get_terminal_size +except ImportError: + def get_terminal_size(fallback=(80, 24)): + try: + import fcntl + import termios + import struct + except ImportError: + return fallback + else: + try: + # This should work on Linux. + res = struct.unpack( + 'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, '1234')) + return (res[1], res[0]) + except Exception: + return fallback diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psaix.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psaix.py new file mode 100644 index 000000000..79e3be15a --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psaix.py @@ -0,0 +1,554 @@ +# Copyright (c) 2009, Giampaolo Rodola' +# Copyright (c) 2017, Arnon Yaari +# All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""AIX platform implementation.""" + +import functools +import glob +import os +import re +import subprocess +import sys +from collections import namedtuple + +from . import _common +from . import _psposix +from . import _psutil_aix as cext +from . import _psutil_posix as cext_posix +from ._common import conn_to_ntuple +from ._common import get_procfs_path +from ._common import memoize_when_activated +from ._common import NIC_DUPLEX_FULL +from ._common import NIC_DUPLEX_HALF +from ._common import NIC_DUPLEX_UNKNOWN +from ._common import usage_percent +from ._compat import FileNotFoundError +from ._compat import PermissionError +from ._compat import ProcessLookupError +from ._compat import PY3 + + +__extra__all__ = ["PROCFS_PATH"] + + +# ===================================================================== +# --- globals +# ===================================================================== + + +HAS_THREADS = hasattr(cext, "proc_threads") +HAS_NET_IO_COUNTERS = hasattr(cext, "net_io_counters") +HAS_PROC_IO_COUNTERS = hasattr(cext, "proc_io_counters") + +PAGE_SIZE = os.sysconf('SC_PAGE_SIZE') +AF_LINK = cext_posix.AF_LINK + +PROC_STATUSES = { + cext.SIDL: _common.STATUS_IDLE, + cext.SZOMB: _common.STATUS_ZOMBIE, + cext.SACTIVE: _common.STATUS_RUNNING, + cext.SSWAP: _common.STATUS_RUNNING, # TODO what status is this? + cext.SSTOP: _common.STATUS_STOPPED, +} + +TCP_STATUSES = { + cext.TCPS_ESTABLISHED: _common.CONN_ESTABLISHED, + cext.TCPS_SYN_SENT: _common.CONN_SYN_SENT, + cext.TCPS_SYN_RCVD: _common.CONN_SYN_RECV, + cext.TCPS_FIN_WAIT_1: _common.CONN_FIN_WAIT1, + cext.TCPS_FIN_WAIT_2: _common.CONN_FIN_WAIT2, + cext.TCPS_TIME_WAIT: _common.CONN_TIME_WAIT, + cext.TCPS_CLOSED: _common.CONN_CLOSE, + cext.TCPS_CLOSE_WAIT: _common.CONN_CLOSE_WAIT, + cext.TCPS_LAST_ACK: _common.CONN_LAST_ACK, + cext.TCPS_LISTEN: _common.CONN_LISTEN, + cext.TCPS_CLOSING: _common.CONN_CLOSING, + cext.PSUTIL_CONN_NONE: _common.CONN_NONE, +} + +proc_info_map = dict( + ppid=0, + rss=1, + vms=2, + create_time=3, + nice=4, + num_threads=5, + status=6, + ttynr=7) + +# These objects get set on "import psutil" from the __init__.py +# file, see: https://github.com/giampaolo/psutil/issues/1402 +NoSuchProcess = None +ZombieProcess = None +AccessDenied = None +TimeoutExpired = None + + +# ===================================================================== +# --- named tuples +# ===================================================================== + + +# psutil.Process.memory_info() +pmem = namedtuple('pmem', ['rss', 'vms']) +# psutil.Process.memory_full_info() +pfullmem = pmem +# psutil.Process.cpu_times() +scputimes = namedtuple('scputimes', ['user', 'system', 'idle', 'iowait']) +# psutil.virtual_memory() +svmem = namedtuple('svmem', ['total', 'available', 'percent', 'used', 'free']) + + +# ===================================================================== +# --- memory +# ===================================================================== + + +def virtual_memory(): + total, avail, free, pinned, inuse = cext.virtual_mem() + percent = usage_percent((total - avail), total, round_=1) + return svmem(total, avail, percent, inuse, free) + + +def swap_memory(): + """Swap system memory as a (total, used, free, sin, sout) tuple.""" + total, free, sin, sout = cext.swap_mem() + used = total - free + percent = usage_percent(used, total, round_=1) + return _common.sswap(total, used, free, percent, sin, sout) + + +# ===================================================================== +# --- CPU +# ===================================================================== + + +def cpu_times(): + """Return system-wide CPU times as a named tuple""" + ret = cext.per_cpu_times() + return scputimes(*[sum(x) for x in zip(*ret)]) + + +def per_cpu_times(): + """Return system per-CPU times as a list of named tuples""" + ret = cext.per_cpu_times() + return [scputimes(*x) for x in ret] + + +def cpu_count_logical(): + """Return the number of logical CPUs in the system.""" + try: + return os.sysconf("SC_NPROCESSORS_ONLN") + except ValueError: + # mimic os.cpu_count() behavior + return None + + +def cpu_count_physical(): + cmd = "lsdev -Cc processor" + p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + if PY3: + stdout, stderr = [x.decode(sys.stdout.encoding) + for x in (stdout, stderr)] + if p.returncode != 0: + raise RuntimeError("%r command error\n%s" % (cmd, stderr)) + processors = stdout.strip().splitlines() + return len(processors) or None + + +def cpu_stats(): + """Return various CPU stats as a named tuple.""" + ctx_switches, interrupts, soft_interrupts, syscalls = cext.cpu_stats() + return _common.scpustats( + ctx_switches, interrupts, soft_interrupts, syscalls) + + +# ===================================================================== +# --- disks +# ===================================================================== + + +disk_io_counters = cext.disk_io_counters +disk_usage = _psposix.disk_usage + + +def disk_partitions(all=False): + """Return system disk partitions.""" + # TODO - the filtering logic should be better checked so that + # it tries to reflect 'df' as much as possible + retlist = [] + partitions = cext.disk_partitions() + for partition in partitions: + device, mountpoint, fstype, opts = partition + if device == 'none': + device = '' + if not all: + # Differently from, say, Linux, we don't have a list of + # common fs types so the best we can do, AFAIK, is to + # filter by filesystem having a total size > 0. + if not disk_usage(mountpoint).total: + continue + ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + retlist.append(ntuple) + return retlist + + +# ===================================================================== +# --- network +# ===================================================================== + + +net_if_addrs = cext_posix.net_if_addrs + +if HAS_NET_IO_COUNTERS: + net_io_counters = cext.net_io_counters + + +def net_connections(kind, _pid=-1): + """Return socket connections. If pid == -1 return system-wide + connections (as opposed to connections opened by one process only). + """ + cmap = _common.conn_tmap + if kind not in cmap: + raise ValueError("invalid %r kind argument; choose between %s" + % (kind, ', '.join([repr(x) for x in cmap]))) + families, types = _common.conn_tmap[kind] + rawlist = cext.net_connections(_pid) + ret = [] + for item in rawlist: + fd, fam, type_, laddr, raddr, status, pid = item + if fam not in families: + continue + if type_ not in types: + continue + nt = conn_to_ntuple(fd, fam, type_, laddr, raddr, status, + TCP_STATUSES, pid=pid if _pid == -1 else None) + ret.append(nt) + return ret + + +def net_if_stats(): + """Get NIC stats (isup, duplex, speed, mtu).""" + duplex_map = {"Full": NIC_DUPLEX_FULL, + "Half": NIC_DUPLEX_HALF} + names = set([x[0] for x in net_if_addrs()]) + ret = {} + for name in names: + isup, mtu = cext.net_if_stats(name) + + # try to get speed and duplex + # TODO: rewrite this in C (entstat forks, so use truss -f to follow. + # looks like it is using an undocumented ioctl?) + duplex = "" + speed = 0 + p = subprocess.Popen(["/usr/bin/entstat", "-d", name], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + if PY3: + stdout, stderr = [x.decode(sys.stdout.encoding) + for x in (stdout, stderr)] + if p.returncode == 0: + re_result = re.search( + r"Running: (\d+) Mbps.*?(\w+) Duplex", stdout) + if re_result is not None: + speed = int(re_result.group(1)) + duplex = re_result.group(2) + + duplex = duplex_map.get(duplex, NIC_DUPLEX_UNKNOWN) + ret[name] = _common.snicstats(isup, duplex, speed, mtu) + return ret + + +# ===================================================================== +# --- other system functions +# ===================================================================== + + +def boot_time(): + """The system boot time expressed in seconds since the epoch.""" + return cext.boot_time() + + +def users(): + """Return currently connected users as a list of namedtuples.""" + retlist = [] + rawlist = cext.users() + localhost = (':0.0', ':0') + for item in rawlist: + user, tty, hostname, tstamp, user_process, pid = item + # note: the underlying C function includes entries about + # system boot, run level and others. We might want + # to use them in the future. + if not user_process: + continue + if hostname in localhost: + hostname = 'localhost' + nt = _common.suser(user, tty, hostname, tstamp, pid) + retlist.append(nt) + return retlist + + +# ===================================================================== +# --- processes +# ===================================================================== + + +def pids(): + """Returns a list of PIDs currently running on the system.""" + return [int(x) for x in os.listdir(get_procfs_path()) if x.isdigit()] + + +def pid_exists(pid): + """Check for the existence of a unix pid.""" + return os.path.exists(os.path.join(get_procfs_path(), str(pid), "psinfo")) + + +def wrap_exceptions(fun): + """Call callable into a try/except clause and translate ENOENT, + EACCES and EPERM in NoSuchProcess or AccessDenied exceptions. + """ + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + try: + return fun(self, *args, **kwargs) + except (FileNotFoundError, ProcessLookupError): + # ENOENT (no such file or directory) gets raised on open(). + # ESRCH (no such process) can get raised on read() if + # process is gone in meantime. + if not pid_exists(self.pid): + raise NoSuchProcess(self.pid, self._name) + else: + raise ZombieProcess(self.pid, self._name, self._ppid) + except PermissionError: + raise AccessDenied(self.pid, self._name) + return wrapper + + +class Process(object): + """Wrapper class around underlying C implementation.""" + + __slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"] + + def __init__(self, pid): + self.pid = pid + self._name = None + self._ppid = None + self._procfs_path = get_procfs_path() + + def oneshot_enter(self): + self._proc_basic_info.cache_activate(self) + self._proc_cred.cache_activate(self) + + def oneshot_exit(self): + self._proc_basic_info.cache_deactivate(self) + self._proc_cred.cache_deactivate(self) + + @wrap_exceptions + @memoize_when_activated + def _proc_basic_info(self): + return cext.proc_basic_info(self.pid, self._procfs_path) + + @wrap_exceptions + @memoize_when_activated + def _proc_cred(self): + return cext.proc_cred(self.pid, self._procfs_path) + + @wrap_exceptions + def name(self): + if self.pid == 0: + return "swapper" + # note: max 16 characters + return cext.proc_name(self.pid, self._procfs_path).rstrip("\x00") + + @wrap_exceptions + def exe(self): + # there is no way to get executable path in AIX other than to guess, + # and guessing is more complex than what's in the wrapping class + cmdline = self.cmdline() + if not cmdline: + return '' + exe = cmdline[0] + if os.path.sep in exe: + # relative or absolute path + if not os.path.isabs(exe): + # if cwd has changed, we're out of luck - this may be wrong! + exe = os.path.abspath(os.path.join(self.cwd(), exe)) + if (os.path.isabs(exe) and + os.path.isfile(exe) and + os.access(exe, os.X_OK)): + return exe + # not found, move to search in PATH using basename only + exe = os.path.basename(exe) + # search for exe name PATH + for path in os.environ["PATH"].split(":"): + possible_exe = os.path.abspath(os.path.join(path, exe)) + if (os.path.isfile(possible_exe) and + os.access(possible_exe, os.X_OK)): + return possible_exe + return '' + + @wrap_exceptions + def cmdline(self): + return cext.proc_args(self.pid) + + @wrap_exceptions + def environ(self): + return cext.proc_environ(self.pid) + + @wrap_exceptions + def create_time(self): + return self._proc_basic_info()[proc_info_map['create_time']] + + @wrap_exceptions + def num_threads(self): + return self._proc_basic_info()[proc_info_map['num_threads']] + + if HAS_THREADS: + @wrap_exceptions + def threads(self): + rawlist = cext.proc_threads(self.pid) + retlist = [] + for thread_id, utime, stime in rawlist: + ntuple = _common.pthread(thread_id, utime, stime) + retlist.append(ntuple) + # The underlying C implementation retrieves all OS threads + # and filters them by PID. At this point we can't tell whether + # an empty list means there were no connections for process or + # process is no longer active so we force NSP in case the PID + # is no longer there. + if not retlist: + # will raise NSP if process is gone + os.stat('%s/%s' % (self._procfs_path, self.pid)) + return retlist + + @wrap_exceptions + def connections(self, kind='inet'): + ret = net_connections(kind, _pid=self.pid) + # The underlying C implementation retrieves all OS connections + # and filters them by PID. At this point we can't tell whether + # an empty list means there were no connections for process or + # process is no longer active so we force NSP in case the PID + # is no longer there. + if not ret: + # will raise NSP if process is gone + os.stat('%s/%s' % (self._procfs_path, self.pid)) + return ret + + @wrap_exceptions + def nice_get(self): + return cext_posix.getpriority(self.pid) + + @wrap_exceptions + def nice_set(self, value): + return cext_posix.setpriority(self.pid, value) + + @wrap_exceptions + def ppid(self): + self._ppid = self._proc_basic_info()[proc_info_map['ppid']] + return self._ppid + + @wrap_exceptions + def uids(self): + real, effective, saved, _, _, _ = self._proc_cred() + return _common.puids(real, effective, saved) + + @wrap_exceptions + def gids(self): + _, _, _, real, effective, saved = self._proc_cred() + return _common.puids(real, effective, saved) + + @wrap_exceptions + def cpu_times(self): + cpu_times = cext.proc_cpu_times(self.pid, self._procfs_path) + return _common.pcputimes(*cpu_times) + + @wrap_exceptions + def terminal(self): + ttydev = self._proc_basic_info()[proc_info_map['ttynr']] + # convert from 64-bit dev_t to 32-bit dev_t and then map the device + ttydev = (((ttydev & 0x0000FFFF00000000) >> 16) | (ttydev & 0xFFFF)) + # try to match rdev of /dev/pts/* files ttydev + for dev in glob.glob("/dev/**/*"): + if os.stat(dev).st_rdev == ttydev: + return dev + return None + + @wrap_exceptions + def cwd(self): + procfs_path = self._procfs_path + try: + result = os.readlink("%s/%s/cwd" % (procfs_path, self.pid)) + return result.rstrip('/') + except FileNotFoundError: + os.stat("%s/%s" % (procfs_path, self.pid)) # raise NSP or AD + return None + + @wrap_exceptions + def memory_info(self): + ret = self._proc_basic_info() + rss = ret[proc_info_map['rss']] * 1024 + vms = ret[proc_info_map['vms']] * 1024 + return pmem(rss, vms) + + memory_full_info = memory_info + + @wrap_exceptions + def status(self): + code = self._proc_basic_info()[proc_info_map['status']] + # XXX is '?' legit? (we're not supposed to return it anyway) + return PROC_STATUSES.get(code, '?') + + def open_files(self): + # TODO rewrite without using procfiles (stat /proc/pid/fd/* and then + # find matching name of the inode) + p = subprocess.Popen(["/usr/bin/procfiles", "-n", str(self.pid)], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + if PY3: + stdout, stderr = [x.decode(sys.stdout.encoding) + for x in (stdout, stderr)] + if "no such process" in stderr.lower(): + raise NoSuchProcess(self.pid, self._name) + procfiles = re.findall(r"(\d+): S_IFREG.*\s*.*name:(.*)\n", stdout) + retlist = [] + for fd, path in procfiles: + path = path.strip() + if path.startswith("//"): + path = path[1:] + if path.lower() == "cannot be retrieved": + continue + retlist.append(_common.popenfile(path, int(fd))) + return retlist + + @wrap_exceptions + def num_fds(self): + if self.pid == 0: # no /proc/0/fd + return 0 + return len(os.listdir("%s/%s/fd" % (self._procfs_path, self.pid))) + + @wrap_exceptions + def num_ctx_switches(self): + return _common.pctxsw( + *cext.proc_num_ctx_switches(self.pid)) + + @wrap_exceptions + def wait(self, timeout=None): + return _psposix.wait_pid(self.pid, timeout, self._name) + + if HAS_PROC_IO_COUNTERS: + @wrap_exceptions + def io_counters(self): + try: + rc, wc, rb, wb = cext.proc_io_counters(self.pid) + except OSError: + # if process is terminated, proc_io_counters returns OSError + # instead of NSP + if not pid_exists(self.pid): + raise NoSuchProcess(self.pid, self._name) + raise + return _common.pio(rc, wc, rb, wb) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psbsd.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psbsd.py new file mode 100644 index 000000000..2f41dc0be --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psbsd.py @@ -0,0 +1,905 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""FreeBSD, OpenBSD and NetBSD platforms implementation.""" + +import contextlib +import errno +import functools +import os +import xml.etree.ElementTree as ET +from collections import namedtuple +from collections import defaultdict + +from . import _common +from . import _psposix +from . import _psutil_bsd as cext +from . import _psutil_posix as cext_posix +from ._common import conn_tmap +from ._common import conn_to_ntuple +from ._common import FREEBSD +from ._common import memoize +from ._common import memoize_when_activated +from ._common import NETBSD +from ._common import OPENBSD +from ._common import usage_percent +from ._compat import FileNotFoundError +from ._compat import PermissionError +from ._compat import ProcessLookupError +from ._compat import which + + +__extra__all__ = [] + + +# ===================================================================== +# --- globals +# ===================================================================== + + +if FREEBSD: + PROC_STATUSES = { + cext.SIDL: _common.STATUS_IDLE, + cext.SRUN: _common.STATUS_RUNNING, + cext.SSLEEP: _common.STATUS_SLEEPING, + cext.SSTOP: _common.STATUS_STOPPED, + cext.SZOMB: _common.STATUS_ZOMBIE, + cext.SWAIT: _common.STATUS_WAITING, + cext.SLOCK: _common.STATUS_LOCKED, + } +elif OPENBSD or NETBSD: + PROC_STATUSES = { + cext.SIDL: _common.STATUS_IDLE, + cext.SSLEEP: _common.STATUS_SLEEPING, + cext.SSTOP: _common.STATUS_STOPPED, + # According to /usr/include/sys/proc.h SZOMB is unused. + # test_zombie_process() shows that SDEAD is the right + # equivalent. Also it appears there's no equivalent of + # psutil.STATUS_DEAD. SDEAD really means STATUS_ZOMBIE. + # cext.SZOMB: _common.STATUS_ZOMBIE, + cext.SDEAD: _common.STATUS_ZOMBIE, + cext.SZOMB: _common.STATUS_ZOMBIE, + # From http://www.eecs.harvard.edu/~margo/cs161/videos/proc.h.txt + # OpenBSD has SRUN and SONPROC: SRUN indicates that a process + # is runnable but *not* yet running, i.e. is on a run queue. + # SONPROC indicates that the process is actually executing on + # a CPU, i.e. it is no longer on a run queue. + # As such we'll map SRUN to STATUS_WAKING and SONPROC to + # STATUS_RUNNING + cext.SRUN: _common.STATUS_WAKING, + cext.SONPROC: _common.STATUS_RUNNING, + } +elif NETBSD: + PROC_STATUSES = { + cext.SIDL: _common.STATUS_IDLE, + cext.SACTIVE: _common.STATUS_RUNNING, + cext.SDYING: _common.STATUS_ZOMBIE, + cext.SSTOP: _common.STATUS_STOPPED, + cext.SZOMB: _common.STATUS_ZOMBIE, + cext.SDEAD: _common.STATUS_DEAD, + cext.SSUSPENDED: _common.STATUS_SUSPENDED, # unique to NetBSD + } + +TCP_STATUSES = { + cext.TCPS_ESTABLISHED: _common.CONN_ESTABLISHED, + cext.TCPS_SYN_SENT: _common.CONN_SYN_SENT, + cext.TCPS_SYN_RECEIVED: _common.CONN_SYN_RECV, + cext.TCPS_FIN_WAIT_1: _common.CONN_FIN_WAIT1, + cext.TCPS_FIN_WAIT_2: _common.CONN_FIN_WAIT2, + cext.TCPS_TIME_WAIT: _common.CONN_TIME_WAIT, + cext.TCPS_CLOSED: _common.CONN_CLOSE, + cext.TCPS_CLOSE_WAIT: _common.CONN_CLOSE_WAIT, + cext.TCPS_LAST_ACK: _common.CONN_LAST_ACK, + cext.TCPS_LISTEN: _common.CONN_LISTEN, + cext.TCPS_CLOSING: _common.CONN_CLOSING, + cext.PSUTIL_CONN_NONE: _common.CONN_NONE, +} + +if NETBSD: + PAGESIZE = os.sysconf("SC_PAGESIZE") +else: + PAGESIZE = os.sysconf("SC_PAGE_SIZE") +AF_LINK = cext_posix.AF_LINK + +HAS_PER_CPU_TIMES = hasattr(cext, "per_cpu_times") +HAS_PROC_NUM_THREADS = hasattr(cext, "proc_num_threads") +HAS_PROC_OPEN_FILES = hasattr(cext, 'proc_open_files') +HAS_PROC_NUM_FDS = hasattr(cext, 'proc_num_fds') + +kinfo_proc_map = dict( + ppid=0, + status=1, + real_uid=2, + effective_uid=3, + saved_uid=4, + real_gid=5, + effective_gid=6, + saved_gid=7, + ttynr=8, + create_time=9, + ctx_switches_vol=10, + ctx_switches_unvol=11, + read_io_count=12, + write_io_count=13, + user_time=14, + sys_time=15, + ch_user_time=16, + ch_sys_time=17, + rss=18, + vms=19, + memtext=20, + memdata=21, + memstack=22, + cpunum=23, + name=24, +) + +# These objects get set on "import psutil" from the __init__.py +# file, see: https://github.com/giampaolo/psutil/issues/1402 +NoSuchProcess = None +ZombieProcess = None +AccessDenied = None +TimeoutExpired = None + + +# ===================================================================== +# --- named tuples +# ===================================================================== + + +# psutil.virtual_memory() +svmem = namedtuple( + 'svmem', ['total', 'available', 'percent', 'used', 'free', + 'active', 'inactive', 'buffers', 'cached', 'shared', 'wired']) +# psutil.cpu_times() +scputimes = namedtuple( + 'scputimes', ['user', 'nice', 'system', 'idle', 'irq']) +# psutil.Process.memory_info() +pmem = namedtuple('pmem', ['rss', 'vms', 'text', 'data', 'stack']) +# psutil.Process.memory_full_info() +pfullmem = pmem +# psutil.Process.cpu_times() +pcputimes = namedtuple('pcputimes', + ['user', 'system', 'children_user', 'children_system']) +# psutil.Process.memory_maps(grouped=True) +pmmap_grouped = namedtuple( + 'pmmap_grouped', 'path rss, private, ref_count, shadow_count') +# psutil.Process.memory_maps(grouped=False) +pmmap_ext = namedtuple( + 'pmmap_ext', 'addr, perms path rss, private, ref_count, shadow_count') +# psutil.disk_io_counters() +if FREEBSD: + sdiskio = namedtuple('sdiskio', ['read_count', 'write_count', + 'read_bytes', 'write_bytes', + 'read_time', 'write_time', + 'busy_time']) +else: + sdiskio = namedtuple('sdiskio', ['read_count', 'write_count', + 'read_bytes', 'write_bytes']) + + +# ===================================================================== +# --- memory +# ===================================================================== + + +def virtual_memory(): + """System virtual memory as a namedtuple.""" + mem = cext.virtual_mem() + total, free, active, inactive, wired, cached, buffers, shared = mem + if NETBSD: + # On NetBSD buffers and shared mem is determined via /proc. + # The C ext set them to 0. + with open('/proc/meminfo', 'rb') as f: + for line in f: + if line.startswith(b'Buffers:'): + buffers = int(line.split()[1]) * 1024 + elif line.startswith(b'MemShared:'): + shared = int(line.split()[1]) * 1024 + avail = inactive + cached + free + used = active + wired + cached + percent = usage_percent((total - avail), total, round_=1) + return svmem(total, avail, percent, used, free, + active, inactive, buffers, cached, shared, wired) + + +def swap_memory(): + """System swap memory as (total, used, free, sin, sout) namedtuple.""" + total, used, free, sin, sout = cext.swap_mem() + percent = usage_percent(used, total, round_=1) + return _common.sswap(total, used, free, percent, sin, sout) + + +# ===================================================================== +# --- CPU +# ===================================================================== + + +def cpu_times(): + """Return system per-CPU times as a namedtuple""" + user, nice, system, idle, irq = cext.cpu_times() + return scputimes(user, nice, system, idle, irq) + + +if HAS_PER_CPU_TIMES: + def per_cpu_times(): + """Return system CPU times as a namedtuple""" + ret = [] + for cpu_t in cext.per_cpu_times(): + user, nice, system, idle, irq = cpu_t + item = scputimes(user, nice, system, idle, irq) + ret.append(item) + return ret +else: + # XXX + # Ok, this is very dirty. + # On FreeBSD < 8 we cannot gather per-cpu information, see: + # https://github.com/giampaolo/psutil/issues/226 + # If num cpus > 1, on first call we return single cpu times to avoid a + # crash at psutil import time. + # Next calls will fail with NotImplementedError + def per_cpu_times(): + """Return system CPU times as a namedtuple""" + if cpu_count_logical() == 1: + return [cpu_times()] + if per_cpu_times.__called__: + raise NotImplementedError("supported only starting from FreeBSD 8") + per_cpu_times.__called__ = True + return [cpu_times()] + + per_cpu_times.__called__ = False + + +def cpu_count_logical(): + """Return the number of logical CPUs in the system.""" + return cext.cpu_count_logical() + + +if OPENBSD or NETBSD: + def cpu_count_physical(): + # OpenBSD and NetBSD do not implement this. + return 1 if cpu_count_logical() == 1 else None +else: + def cpu_count_physical(): + """Return the number of physical CPUs in the system.""" + # From the C module we'll get an XML string similar to this: + # http://manpages.ubuntu.com/manpages/precise/man4/smp.4freebsd.html + # We may get None in case "sysctl kern.sched.topology_spec" + # is not supported on this BSD version, in which case we'll mimic + # os.cpu_count() and return None. + ret = None + s = cext.cpu_count_phys() + if s is not None: + # get rid of padding chars appended at the end of the string + index = s.rfind("") + if index != -1: + s = s[:index + 9] + root = ET.fromstring(s) + try: + ret = len(root.findall('group/children/group/cpu')) or None + finally: + # needed otherwise it will memleak + root.clear() + if not ret: + # If logical CPUs are 1 it's obvious we'll have only 1 + # physical CPU. + if cpu_count_logical() == 1: + return 1 + return ret + + +def cpu_stats(): + """Return various CPU stats as a named tuple.""" + if FREEBSD: + # Note: the C ext is returning some metrics we are not exposing: + # traps. + ctxsw, intrs, soft_intrs, syscalls, traps = cext.cpu_stats() + elif NETBSD: + # XXX + # Note about intrs: the C extension returns 0. intrs + # can be determined via /proc/stat; it has the same value as + # soft_intrs thought so the kernel is faking it (?). + # + # Note about syscalls: the C extension always sets it to 0 (?). + # + # Note: the C ext is returning some metrics we are not exposing: + # traps, faults and forks. + ctxsw, intrs, soft_intrs, syscalls, traps, faults, forks = \ + cext.cpu_stats() + with open('/proc/stat', 'rb') as f: + for line in f: + if line.startswith(b'intr'): + intrs = int(line.split()[1]) + elif OPENBSD: + # Note: the C ext is returning some metrics we are not exposing: + # traps, faults and forks. + ctxsw, intrs, soft_intrs, syscalls, traps, faults, forks = \ + cext.cpu_stats() + return _common.scpustats(ctxsw, intrs, soft_intrs, syscalls) + + +# ===================================================================== +# --- disks +# ===================================================================== + + +def disk_partitions(all=False): + """Return mounted disk partitions as a list of namedtuples. + 'all' argument is ignored, see: + https://github.com/giampaolo/psutil/issues/906 + """ + retlist = [] + partitions = cext.disk_partitions() + for partition in partitions: + device, mountpoint, fstype, opts = partition + ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + retlist.append(ntuple) + return retlist + + +disk_usage = _psposix.disk_usage +disk_io_counters = cext.disk_io_counters + + +# ===================================================================== +# --- network +# ===================================================================== + + +net_io_counters = cext.net_io_counters +net_if_addrs = cext_posix.net_if_addrs + + +def net_if_stats(): + """Get NIC stats (isup, duplex, speed, mtu).""" + names = net_io_counters().keys() + ret = {} + for name in names: + try: + mtu = cext_posix.net_if_mtu(name) + isup = cext_posix.net_if_flags(name) + duplex, speed = cext_posix.net_if_duplex_speed(name) + except OSError as err: + # https://github.com/giampaolo/psutil/issues/1279 + if err.errno != errno.ENODEV: + raise + else: + if hasattr(_common, 'NicDuplex'): + duplex = _common.NicDuplex(duplex) + ret[name] = _common.snicstats(isup, duplex, speed, mtu) + return ret + + +def net_connections(kind): + """System-wide network connections.""" + if OPENBSD: + ret = [] + for pid in pids(): + try: + cons = Process(pid).connections(kind) + except (NoSuchProcess, ZombieProcess): + continue + else: + for conn in cons: + conn = list(conn) + conn.append(pid) + ret.append(_common.sconn(*conn)) + return ret + + if kind not in _common.conn_tmap: + raise ValueError("invalid %r kind argument; choose between %s" + % (kind, ', '.join([repr(x) for x in conn_tmap]))) + families, types = conn_tmap[kind] + ret = set() + if NETBSD: + rawlist = cext.net_connections(-1) + else: + rawlist = cext.net_connections() + for item in rawlist: + fd, fam, type, laddr, raddr, status, pid = item + # TODO: apply filter at C level + if fam in families and type in types: + nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, + TCP_STATUSES, pid) + ret.add(nt) + return list(ret) + + +# ===================================================================== +# --- sensors +# ===================================================================== + + +if FREEBSD: + + def sensors_battery(): + """Return battery info.""" + try: + percent, minsleft, power_plugged = cext.sensors_battery() + except NotImplementedError: + # See: https://github.com/giampaolo/psutil/issues/1074 + return None + power_plugged = power_plugged == 1 + if power_plugged: + secsleft = _common.POWER_TIME_UNLIMITED + elif minsleft == -1: + secsleft = _common.POWER_TIME_UNKNOWN + else: + secsleft = minsleft * 60 + return _common.sbattery(percent, secsleft, power_plugged) + + def sensors_temperatures(): + "Return CPU cores temperatures if available, else an empty dict." + ret = defaultdict(list) + num_cpus = cpu_count_logical() + for cpu in range(num_cpus): + try: + current, high = cext.sensors_cpu_temperature(cpu) + if high <= 0: + high = None + name = "Core %s" % cpu + ret["coretemp"].append( + _common.shwtemp(name, current, high, high)) + except NotImplementedError: + pass + + return ret + + def cpu_freq(): + """Return frequency metrics for CPUs. As of Dec 2018 only + CPU 0 appears to be supported by FreeBSD and all other cores + match the frequency of CPU 0. + """ + ret = [] + num_cpus = cpu_count_logical() + for cpu in range(num_cpus): + try: + current, available_freq = cext.cpu_frequency(cpu) + except NotImplementedError: + continue + if available_freq: + try: + min_freq = int(available_freq.split(" ")[-1].split("/")[0]) + except(IndexError, ValueError): + min_freq = None + try: + max_freq = int(available_freq.split(" ")[0].split("/")[0]) + except(IndexError, ValueError): + max_freq = None + ret.append(_common.scpufreq(current, min_freq, max_freq)) + return ret + + +# ===================================================================== +# --- other system functions +# ===================================================================== + + +def boot_time(): + """The system boot time expressed in seconds since the epoch.""" + return cext.boot_time() + + +def users(): + """Return currently connected users as a list of namedtuples.""" + retlist = [] + rawlist = cext.users() + for item in rawlist: + user, tty, hostname, tstamp, pid = item + if pid == -1: + assert OPENBSD + pid = None + if tty == '~': + continue # reboot or shutdown + nt = _common.suser(user, tty or None, hostname, tstamp, pid) + retlist.append(nt) + return retlist + + +# ===================================================================== +# --- processes +# ===================================================================== + + +@memoize +def _pid_0_exists(): + try: + Process(0).name() + except NoSuchProcess: + return False + except AccessDenied: + return True + else: + return True + + +def pids(): + """Returns a list of PIDs currently running on the system.""" + ret = cext.pids() + if OPENBSD and (0 not in ret) and _pid_0_exists(): + # On OpenBSD the kernel does not return PID 0 (neither does + # ps) but it's actually querable (Process(0) will succeed). + ret.insert(0, 0) + return ret + + +if OPENBSD or NETBSD: + def pid_exists(pid): + """Return True if pid exists.""" + exists = _psposix.pid_exists(pid) + if not exists: + # We do this because _psposix.pid_exists() lies in case of + # zombie processes. + return pid in pids() + else: + return True +else: + pid_exists = _psposix.pid_exists + + +def is_zombie(pid): + try: + st = cext.proc_oneshot_info(pid)[kinfo_proc_map['status']] + return st == cext.SZOMB + except Exception: + return False + + +def wrap_exceptions(fun): + """Decorator which translates bare OSError exceptions into + NoSuchProcess and AccessDenied. + """ + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + try: + return fun(self, *args, **kwargs) + except ProcessLookupError: + if not pid_exists(self.pid): + raise NoSuchProcess(self.pid, self._name) + else: + raise ZombieProcess(self.pid, self._name, self._ppid) + except PermissionError: + raise AccessDenied(self.pid, self._name) + except OSError: + if self.pid == 0: + if 0 in pids(): + raise AccessDenied(self.pid, self._name) + else: + raise + raise + return wrapper + + +@contextlib.contextmanager +def wrap_exceptions_procfs(inst): + """Same as above, for routines relying on reading /proc fs.""" + try: + yield + except (ProcessLookupError, FileNotFoundError): + # ENOENT (no such file or directory) gets raised on open(). + # ESRCH (no such process) can get raised on read() if + # process is gone in meantime. + if not pid_exists(inst.pid): + raise NoSuchProcess(inst.pid, inst._name) + else: + raise ZombieProcess(inst.pid, inst._name, inst._ppid) + except PermissionError: + raise AccessDenied(inst.pid, inst._name) + + +class Process(object): + """Wrapper class around underlying C implementation.""" + + __slots__ = ["pid", "_name", "_ppid", "_cache"] + + def __init__(self, pid): + self.pid = pid + self._name = None + self._ppid = None + + def _assert_alive(self): + """Raise NSP if the process disappeared on us.""" + # For those C function who do not raise NSP, possibly returning + # incorrect or incomplete result. + cext.proc_name(self.pid) + + @wrap_exceptions + @memoize_when_activated + def oneshot(self): + """Retrieves multiple process info in one shot as a raw tuple.""" + ret = cext.proc_oneshot_info(self.pid) + assert len(ret) == len(kinfo_proc_map) + return ret + + def oneshot_enter(self): + self.oneshot.cache_activate(self) + + def oneshot_exit(self): + self.oneshot.cache_deactivate(self) + + @wrap_exceptions + def name(self): + name = self.oneshot()[kinfo_proc_map['name']] + return name if name is not None else cext.proc_name(self.pid) + + @wrap_exceptions + def exe(self): + if FREEBSD: + return cext.proc_exe(self.pid) + elif NETBSD: + if self.pid == 0: + # /proc/0 dir exists but /proc/0/exe doesn't + return "" + with wrap_exceptions_procfs(self): + return os.readlink("/proc/%s/exe" % self.pid) + else: + # OpenBSD: exe cannot be determined; references: + # https://chromium.googlesource.com/chromium/src/base/+/ + # master/base_paths_posix.cc + # We try our best guess by using which against the first + # cmdline arg (may return None). + cmdline = self.cmdline() + if cmdline: + return which(cmdline[0]) + else: + return "" + + @wrap_exceptions + def cmdline(self): + if OPENBSD and self.pid == 0: + return [] # ...else it crashes + elif NETBSD: + # XXX - most of the times the underlying sysctl() call on Net + # and Open BSD returns a truncated string. + # Also /proc/pid/cmdline behaves the same so it looks + # like this is a kernel bug. + try: + return cext.proc_cmdline(self.pid) + except OSError as err: + if err.errno == errno.EINVAL: + if is_zombie(self.pid): + raise ZombieProcess(self.pid, self._name, self._ppid) + elif not pid_exists(self.pid): + raise NoSuchProcess(self.pid, self._name, self._ppid) + else: + # XXX: this happens with unicode tests. It means the C + # routine is unable to decode invalid unicode chars. + return [] + else: + raise + else: + return cext.proc_cmdline(self.pid) + + @wrap_exceptions + def terminal(self): + tty_nr = self.oneshot()[kinfo_proc_map['ttynr']] + tmap = _psposix.get_terminal_map() + try: + return tmap[tty_nr] + except KeyError: + return None + + @wrap_exceptions + def ppid(self): + self._ppid = self.oneshot()[kinfo_proc_map['ppid']] + return self._ppid + + @wrap_exceptions + def uids(self): + rawtuple = self.oneshot() + return _common.puids( + rawtuple[kinfo_proc_map['real_uid']], + rawtuple[kinfo_proc_map['effective_uid']], + rawtuple[kinfo_proc_map['saved_uid']]) + + @wrap_exceptions + def gids(self): + rawtuple = self.oneshot() + return _common.pgids( + rawtuple[kinfo_proc_map['real_gid']], + rawtuple[kinfo_proc_map['effective_gid']], + rawtuple[kinfo_proc_map['saved_gid']]) + + @wrap_exceptions + def cpu_times(self): + rawtuple = self.oneshot() + return _common.pcputimes( + rawtuple[kinfo_proc_map['user_time']], + rawtuple[kinfo_proc_map['sys_time']], + rawtuple[kinfo_proc_map['ch_user_time']], + rawtuple[kinfo_proc_map['ch_sys_time']]) + + if FREEBSD: + @wrap_exceptions + def cpu_num(self): + return self.oneshot()[kinfo_proc_map['cpunum']] + + @wrap_exceptions + def memory_info(self): + rawtuple = self.oneshot() + return pmem( + rawtuple[kinfo_proc_map['rss']], + rawtuple[kinfo_proc_map['vms']], + rawtuple[kinfo_proc_map['memtext']], + rawtuple[kinfo_proc_map['memdata']], + rawtuple[kinfo_proc_map['memstack']]) + + memory_full_info = memory_info + + @wrap_exceptions + def create_time(self): + return self.oneshot()[kinfo_proc_map['create_time']] + + @wrap_exceptions + def num_threads(self): + if HAS_PROC_NUM_THREADS: + # FreeBSD + return cext.proc_num_threads(self.pid) + else: + return len(self.threads()) + + @wrap_exceptions + def num_ctx_switches(self): + rawtuple = self.oneshot() + return _common.pctxsw( + rawtuple[kinfo_proc_map['ctx_switches_vol']], + rawtuple[kinfo_proc_map['ctx_switches_unvol']]) + + @wrap_exceptions + def threads(self): + # Note: on OpenSBD this (/dev/mem) requires root access. + rawlist = cext.proc_threads(self.pid) + retlist = [] + for thread_id, utime, stime in rawlist: + ntuple = _common.pthread(thread_id, utime, stime) + retlist.append(ntuple) + if OPENBSD: + self._assert_alive() + return retlist + + @wrap_exceptions + def connections(self, kind='inet'): + if kind not in conn_tmap: + raise ValueError("invalid %r kind argument; choose between %s" + % (kind, ', '.join([repr(x) for x in conn_tmap]))) + + if NETBSD: + families, types = conn_tmap[kind] + ret = [] + rawlist = cext.net_connections(self.pid) + for item in rawlist: + fd, fam, type, laddr, raddr, status, pid = item + assert pid == self.pid + if fam in families and type in types: + nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, + TCP_STATUSES) + ret.append(nt) + self._assert_alive() + return list(ret) + + families, types = conn_tmap[kind] + rawlist = cext.proc_connections(self.pid, families, types) + ret = [] + for item in rawlist: + fd, fam, type, laddr, raddr, status = item + nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, + TCP_STATUSES) + ret.append(nt) + + if OPENBSD: + self._assert_alive() + + return ret + + @wrap_exceptions + def wait(self, timeout=None): + return _psposix.wait_pid(self.pid, timeout, self._name) + + @wrap_exceptions + def nice_get(self): + return cext_posix.getpriority(self.pid) + + @wrap_exceptions + def nice_set(self, value): + return cext_posix.setpriority(self.pid, value) + + @wrap_exceptions + def status(self): + code = self.oneshot()[kinfo_proc_map['status']] + # XXX is '?' legit? (we're not supposed to return it anyway) + return PROC_STATUSES.get(code, '?') + + @wrap_exceptions + def io_counters(self): + rawtuple = self.oneshot() + return _common.pio( + rawtuple[kinfo_proc_map['read_io_count']], + rawtuple[kinfo_proc_map['write_io_count']], + -1, + -1) + + @wrap_exceptions + def cwd(self): + """Return process current working directory.""" + # sometimes we get an empty string, in which case we turn + # it into None + if OPENBSD and self.pid == 0: + return None # ...else it would raise EINVAL + elif NETBSD or HAS_PROC_OPEN_FILES: + # FreeBSD < 8 does not support functions based on + # kinfo_getfile() and kinfo_getvmmap() + return cext.proc_cwd(self.pid) or None + else: + raise NotImplementedError( + "supported only starting from FreeBSD 8" if + FREEBSD else "") + + nt_mmap_grouped = namedtuple( + 'mmap', 'path rss, private, ref_count, shadow_count') + nt_mmap_ext = namedtuple( + 'mmap', 'addr, perms path rss, private, ref_count, shadow_count') + + def _not_implemented(self): + raise NotImplementedError + + # FreeBSD < 8 does not support functions based on kinfo_getfile() + # and kinfo_getvmmap() + if HAS_PROC_OPEN_FILES: + @wrap_exceptions + def open_files(self): + """Return files opened by process as a list of namedtuples.""" + rawlist = cext.proc_open_files(self.pid) + return [_common.popenfile(path, fd) for path, fd in rawlist] + else: + open_files = _not_implemented + + # FreeBSD < 8 does not support functions based on kinfo_getfile() + # and kinfo_getvmmap() + if HAS_PROC_NUM_FDS: + @wrap_exceptions + def num_fds(self): + """Return the number of file descriptors opened by this process.""" + ret = cext.proc_num_fds(self.pid) + if NETBSD: + self._assert_alive() + return ret + else: + num_fds = _not_implemented + + # --- FreeBSD only APIs + + if FREEBSD: + + @wrap_exceptions + def cpu_affinity_get(self): + return cext.proc_cpu_affinity_get(self.pid) + + @wrap_exceptions + def cpu_affinity_set(self, cpus): + # Pre-emptively check if CPUs are valid because the C + # function has a weird behavior in case of invalid CPUs, + # see: https://github.com/giampaolo/psutil/issues/586 + allcpus = tuple(range(len(per_cpu_times()))) + for cpu in cpus: + if cpu not in allcpus: + raise ValueError("invalid CPU #%i (choose between %s)" + % (cpu, allcpus)) + try: + cext.proc_cpu_affinity_set(self.pid, cpus) + except OSError as err: + # 'man cpuset_setaffinity' about EDEADLK: + # <> + if err.errno in (errno.EINVAL, errno.EDEADLK): + for cpu in cpus: + if cpu not in allcpus: + raise ValueError( + "invalid CPU #%i (choose between %s)" % ( + cpu, allcpus)) + raise + + @wrap_exceptions + def memory_maps(self): + return cext.proc_memory_maps(self.pid) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_pslinux.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_pslinux.py new file mode 100644 index 000000000..a56ead369 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_pslinux.py @@ -0,0 +1,2096 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Linux platform implementation.""" + +from __future__ import division + +import base64 +import collections +import errno +import functools +import glob +import os +import re +import socket +import struct +import sys +import traceback +import warnings +from collections import defaultdict +from collections import namedtuple + +from . import _common +from . import _psposix +from . import _psutil_linux as cext +from . import _psutil_posix as cext_posix +from ._common import decode +from ._common import get_procfs_path +from ._common import isfile_strict +from ._common import memoize +from ._common import memoize_when_activated +from ._common import NIC_DUPLEX_FULL +from ._common import NIC_DUPLEX_HALF +from ._common import NIC_DUPLEX_UNKNOWN +from ._common import open_binary +from ._common import open_text +from ._common import parse_environ_block +from ._common import path_exists_strict +from ._common import supports_ipv6 +from ._common import usage_percent +from ._compat import b +from ._compat import basestring +from ._compat import FileNotFoundError +from ._compat import PermissionError +from ._compat import ProcessLookupError +from ._compat import PY3 + +if sys.version_info >= (3, 4): + import enum +else: + enum = None + + +__extra__all__ = [ + # + 'PROCFS_PATH', + # io prio constants + "IOPRIO_CLASS_NONE", "IOPRIO_CLASS_RT", "IOPRIO_CLASS_BE", + "IOPRIO_CLASS_IDLE", + # connection status constants + "CONN_ESTABLISHED", "CONN_SYN_SENT", "CONN_SYN_RECV", "CONN_FIN_WAIT1", + "CONN_FIN_WAIT2", "CONN_TIME_WAIT", "CONN_CLOSE", "CONN_CLOSE_WAIT", + "CONN_LAST_ACK", "CONN_LISTEN", "CONN_CLOSING", ] + + +# ===================================================================== +# --- globals +# ===================================================================== + + +POWER_SUPPLY_PATH = "/sys/class/power_supply" +HAS_SMAPS = os.path.exists('/proc/%s/smaps' % os.getpid()) +HAS_PRLIMIT = hasattr(cext, "linux_prlimit") +HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_ioprio_get") +HAS_CPU_AFFINITY = hasattr(cext, "proc_cpu_affinity_get") +_DEFAULT = object() + +# RLIMIT_* constants, not guaranteed to be present on all kernels +if HAS_PRLIMIT: + for name in dir(cext): + if name.startswith('RLIM'): + __extra__all__.append(name) + +# Number of clock ticks per second +CLOCK_TICKS = os.sysconf("SC_CLK_TCK") +PAGESIZE = os.sysconf("SC_PAGE_SIZE") +BOOT_TIME = None # set later +# Used when reading "big" files, namely /proc/{pid}/smaps and /proc/net/*. +# On Python 2, using a buffer with open() for such files may result in a +# speedup, see: https://github.com/giampaolo/psutil/issues/708 +BIGFILE_BUFFERING = -1 if PY3 else 8192 +LITTLE_ENDIAN = sys.byteorder == 'little' + +# "man iostat" states that sectors are equivalent with blocks and have +# a size of 512 bytes. Despite this value can be queried at runtime +# via /sys/block/{DISK}/queue/hw_sector_size and results may vary +# between 1k, 2k, or 4k... 512 appears to be a magic constant used +# throughout Linux source code: +# * https://stackoverflow.com/a/38136179/376587 +# * https://lists.gt.net/linux/kernel/2241060 +# * https://github.com/giampaolo/psutil/issues/1305 +# * https://github.com/torvalds/linux/blob/ +# 4f671fe2f9523a1ea206f63fe60a7c7b3a56d5c7/include/linux/bio.h#L99 +# * https://lkml.org/lkml/2015/8/17/234 +DISK_SECTOR_SIZE = 512 + +if enum is None: + AF_LINK = socket.AF_PACKET +else: + AddressFamily = enum.IntEnum('AddressFamily', + {'AF_LINK': int(socket.AF_PACKET)}) + AF_LINK = AddressFamily.AF_LINK + +# ioprio_* constants http://linux.die.net/man/2/ioprio_get +if enum is None: + IOPRIO_CLASS_NONE = 0 + IOPRIO_CLASS_RT = 1 + IOPRIO_CLASS_BE = 2 + IOPRIO_CLASS_IDLE = 3 +else: + class IOPriority(enum.IntEnum): + IOPRIO_CLASS_NONE = 0 + IOPRIO_CLASS_RT = 1 + IOPRIO_CLASS_BE = 2 + IOPRIO_CLASS_IDLE = 3 + + globals().update(IOPriority.__members__) + +# See: +# https://github.com/torvalds/linux/blame/master/fs/proc/array.c +# ...and (TASK_* constants): +# https://github.com/torvalds/linux/blob/master/include/linux/sched.h +PROC_STATUSES = { + "R": _common.STATUS_RUNNING, + "S": _common.STATUS_SLEEPING, + "D": _common.STATUS_DISK_SLEEP, + "T": _common.STATUS_STOPPED, + "t": _common.STATUS_TRACING_STOP, + "Z": _common.STATUS_ZOMBIE, + "X": _common.STATUS_DEAD, + "x": _common.STATUS_DEAD, + "K": _common.STATUS_WAKE_KILL, + "W": _common.STATUS_WAKING, + "I": _common.STATUS_IDLE, + "P": _common.STATUS_PARKED, +} + +# https://github.com/torvalds/linux/blob/master/include/net/tcp_states.h +TCP_STATUSES = { + "01": _common.CONN_ESTABLISHED, + "02": _common.CONN_SYN_SENT, + "03": _common.CONN_SYN_RECV, + "04": _common.CONN_FIN_WAIT1, + "05": _common.CONN_FIN_WAIT2, + "06": _common.CONN_TIME_WAIT, + "07": _common.CONN_CLOSE, + "08": _common.CONN_CLOSE_WAIT, + "09": _common.CONN_LAST_ACK, + "0A": _common.CONN_LISTEN, + "0B": _common.CONN_CLOSING +} + +# These objects get set on "import psutil" from the __init__.py +# file, see: https://github.com/giampaolo/psutil/issues/1402 +NoSuchProcess = None +ZombieProcess = None +AccessDenied = None +TimeoutExpired = None + + +# ===================================================================== +# --- named tuples +# ===================================================================== + + +# psutil.virtual_memory() +svmem = namedtuple( + 'svmem', ['total', 'available', 'percent', 'used', 'free', + 'active', 'inactive', 'buffers', 'cached', 'shared', 'slab']) +# psutil.disk_io_counters() +sdiskio = namedtuple( + 'sdiskio', ['read_count', 'write_count', + 'read_bytes', 'write_bytes', + 'read_time', 'write_time', + 'read_merged_count', 'write_merged_count', + 'busy_time']) +# psutil.Process().open_files() +popenfile = namedtuple( + 'popenfile', ['path', 'fd', 'position', 'mode', 'flags']) +# psutil.Process().memory_info() +pmem = namedtuple('pmem', 'rss vms shared text lib data dirty') +# psutil.Process().memory_full_info() +pfullmem = namedtuple('pfullmem', pmem._fields + ('uss', 'pss', 'swap')) +# psutil.Process().memory_maps(grouped=True) +pmmap_grouped = namedtuple( + 'pmmap_grouped', + ['path', 'rss', 'size', 'pss', 'shared_clean', 'shared_dirty', + 'private_clean', 'private_dirty', 'referenced', 'anonymous', 'swap']) +# psutil.Process().memory_maps(grouped=False) +pmmap_ext = namedtuple( + 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields)) +# psutil.Process.io_counters() +pio = namedtuple('pio', ['read_count', 'write_count', + 'read_bytes', 'write_bytes', + 'read_chars', 'write_chars']) +# psutil.Process.cpu_times() +pcputimes = namedtuple('pcputimes', + ['user', 'system', 'children_user', 'children_system', + 'iowait']) + + +# ===================================================================== +# --- utils +# ===================================================================== + + +def readlink(path): + """Wrapper around os.readlink().""" + assert isinstance(path, basestring), path + path = os.readlink(path) + # readlink() might return paths containing null bytes ('\x00') + # resulting in "TypeError: must be encoded string without NULL + # bytes, not str" errors when the string is passed to other + # fs-related functions (os.*, open(), ...). + # Apparently everything after '\x00' is garbage (we can have + # ' (deleted)', 'new' and possibly others), see: + # https://github.com/giampaolo/psutil/issues/717 + path = path.split('\x00')[0] + # Certain paths have ' (deleted)' appended. Usually this is + # bogus as the file actually exists. Even if it doesn't we + # don't care. + if path.endswith(' (deleted)') and not path_exists_strict(path): + path = path[:-10] + return path + + +def file_flags_to_mode(flags): + """Convert file's open() flags into a readable string. + Used by Process.open_files(). + """ + modes_map = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'} + mode = modes_map[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)] + if flags & os.O_APPEND: + mode = mode.replace('w', 'a', 1) + mode = mode.replace('w+', 'r+') + # possible values: r, w, a, r+, a+ + return mode + + +def is_storage_device(name): + """Return True if the given name refers to a root device (e.g. + "sda", "nvme0n1") as opposed to a logical partition (e.g. "sda1", + "nvme0n1p1"). If name is a virtual device (e.g. "loop1", "ram") + return True. + """ + # Readapted from iostat source code, see: + # https://github.com/sysstat/sysstat/blob/ + # 97912938cd476645b267280069e83b1c8dc0e1c7/common.c#L208 + # Some devices may have a slash in their name (e.g. cciss/c0d0...). + name = name.replace('/', '!') + including_virtual = True + if including_virtual: + path = "/sys/block/%s" % name + else: + path = "/sys/block/%s/device" % name + return os.access(path, os.F_OK) + + +@memoize +def set_scputimes_ntuple(procfs_path): + """Set a namedtuple of variable fields depending on the CPU times + available on this Linux kernel version which may be: + (user, nice, system, idle, iowait, irq, softirq, [steal, [guest, + [guest_nice]]]) + Used by cpu_times() function. + """ + global scputimes + with open_binary('%s/stat' % procfs_path) as f: + values = f.readline().split()[1:] + fields = ['user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq'] + vlen = len(values) + if vlen >= 8: + # Linux >= 2.6.11 + fields.append('steal') + if vlen >= 9: + # Linux >= 2.6.24 + fields.append('guest') + if vlen >= 10: + # Linux >= 3.2.0 + fields.append('guest_nice') + scputimes = namedtuple('scputimes', fields) + + +def cat(fname, fallback=_DEFAULT, binary=True): + """Return file content. + fallback: the value returned in case the file does not exist or + cannot be read + binary: whether to open the file in binary or text mode. + """ + try: + with open_binary(fname) if binary else open_text(fname) as f: + return f.read().strip() + except (IOError, OSError): + if fallback is not _DEFAULT: + return fallback + else: + raise + + +try: + set_scputimes_ntuple("/proc") +except Exception: + # Don't want to crash at import time. + traceback.print_exc() + scputimes = namedtuple('scputimes', 'user system idle')(0.0, 0.0, 0.0) + + +# ===================================================================== +# --- system memory +# ===================================================================== + + +def calculate_avail_vmem(mems): + """Fallback for kernels < 3.14 where /proc/meminfo does not provide + "MemAvailable:" column, see: + https://blog.famzah.net/2014/09/24/ + This code reimplements the algorithm outlined here: + https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/ + commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773 + + XXX: on recent kernels this calculation differs by ~1.5% than + "MemAvailable:" as it's calculated slightly differently, see: + https://gitlab.com/procps-ng/procps/issues/42 + https://github.com/famzah/linux-memavailable-procfs/issues/2 + It is still way more realistic than doing (free + cached) though. + """ + # Fallback for very old distros. According to + # https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/ + # commit/?id=34e431b0ae398fc54ea69ff85ec700722c9da773 + # ...long ago "avail" was calculated as (free + cached). + # We might fallback in such cases: + # "Active(file)" not available: 2.6.28 / Dec 2008 + # "Inactive(file)" not available: 2.6.28 / Dec 2008 + # "SReclaimable:" not available: 2.6.19 / Nov 2006 + # /proc/zoneinfo not available: 2.6.13 / Aug 2005 + free = mems[b'MemFree:'] + fallback = free + mems.get(b"Cached:", 0) + try: + lru_active_file = mems[b'Active(file):'] + lru_inactive_file = mems[b'Inactive(file):'] + slab_reclaimable = mems[b'SReclaimable:'] + except KeyError: + return fallback + try: + f = open_binary('%s/zoneinfo' % get_procfs_path()) + except IOError: + return fallback # kernel 2.6.13 + + watermark_low = 0 + with f: + for line in f: + line = line.strip() + if line.startswith(b'low'): + watermark_low += int(line.split()[1]) + watermark_low *= PAGESIZE + watermark_low = watermark_low + + avail = free - watermark_low + pagecache = lru_active_file + lru_inactive_file + pagecache -= min(pagecache / 2, watermark_low) + avail += pagecache + avail += slab_reclaimable - min(slab_reclaimable / 2.0, watermark_low) + return int(avail) + + +def virtual_memory(): + """Report virtual memory stats. + This implementation matches "free" and "vmstat -s" cmdline + utility values and procps-ng-3.3.12 source was used as a reference + (2016-09-18): + https://gitlab.com/procps-ng/procps/blob/ + 24fd2605c51fccc375ab0287cec33aa767f06718/proc/sysinfo.c + For reference, procps-ng-3.3.10 is the version available on Ubuntu + 16.04. + + Note about "available" memory: up until psutil 4.3 it was + calculated as "avail = (free + buffers + cached)". Now + "MemAvailable:" column (kernel 3.14) from /proc/meminfo is used as + it's more accurate. + That matches "available" column in newer versions of "free". + """ + missing_fields = [] + mems = {} + with open_binary('%s/meminfo' % get_procfs_path()) as f: + for line in f: + fields = line.split() + mems[fields[0]] = int(fields[1]) * 1024 + + # /proc doc states that the available fields in /proc/meminfo vary + # by architecture and compile options, but these 3 values are also + # returned by sysinfo(2); as such we assume they are always there. + total = mems[b'MemTotal:'] + free = mems[b'MemFree:'] + try: + buffers = mems[b'Buffers:'] + except KeyError: + # https://github.com/giampaolo/psutil/issues/1010 + buffers = 0 + missing_fields.append('buffers') + try: + cached = mems[b"Cached:"] + except KeyError: + cached = 0 + missing_fields.append('cached') + else: + # "free" cmdline utility sums reclaimable to cached. + # Older versions of procps used to add slab memory instead. + # This got changed in: + # https://gitlab.com/procps-ng/procps/commit/ + # 05d751c4f076a2f0118b914c5e51cfbb4762ad8e + cached += mems.get(b"SReclaimable:", 0) # since kernel 2.6.19 + + try: + shared = mems[b'Shmem:'] # since kernel 2.6.32 + except KeyError: + try: + shared = mems[b'MemShared:'] # kernels 2.4 + except KeyError: + shared = 0 + missing_fields.append('shared') + + try: + active = mems[b"Active:"] + except KeyError: + active = 0 + missing_fields.append('active') + + try: + inactive = mems[b"Inactive:"] + except KeyError: + try: + inactive = \ + mems[b"Inact_dirty:"] + \ + mems[b"Inact_clean:"] + \ + mems[b"Inact_laundry:"] + except KeyError: + inactive = 0 + missing_fields.append('inactive') + + try: + slab = mems[b"Slab:"] + except KeyError: + slab = 0 + + used = total - free - cached - buffers + if used < 0: + # May be symptomatic of running within a LCX container where such + # values will be dramatically distorted over those of the host. + used = total - free + + # - starting from 4.4.0 we match free's "available" column. + # Before 4.4.0 we calculated it as (free + buffers + cached) + # which matched htop. + # - free and htop available memory differs as per: + # http://askubuntu.com/a/369589 + # http://unix.stackexchange.com/a/65852/168884 + # - MemAvailable has been introduced in kernel 3.14 + try: + avail = mems[b'MemAvailable:'] + except KeyError: + avail = calculate_avail_vmem(mems) + + if avail < 0: + avail = 0 + missing_fields.append('available') + + # If avail is greater than total or our calculation overflows, + # that's symptomatic of running within a LCX container where such + # values will be dramatically distorted over those of the host. + # https://gitlab.com/procps-ng/procps/blob/ + # 24fd2605c51fccc375ab0287cec33aa767f06718/proc/sysinfo.c#L764 + if avail > total: + avail = free + + percent = usage_percent((total - avail), total, round_=1) + + # Warn about missing metrics which are set to 0. + if missing_fields: + msg = "%s memory stats couldn't be determined and %s set to 0" % ( + ", ".join(missing_fields), + "was" if len(missing_fields) == 1 else "were") + warnings.warn(msg, RuntimeWarning) + + return svmem(total, avail, percent, used, free, + active, inactive, buffers, cached, shared, slab) + + +def swap_memory(): + """Return swap memory metrics.""" + mems = {} + with open_binary('%s/meminfo' % get_procfs_path()) as f: + for line in f: + fields = line.split() + mems[fields[0]] = int(fields[1]) * 1024 + # We prefer /proc/meminfo over sysinfo() syscall so that + # psutil.PROCFS_PATH can be used in order to allow retrieval + # for linux containers, see: + # https://github.com/giampaolo/psutil/issues/1015 + try: + total = mems[b'SwapTotal:'] + free = mems[b'SwapFree:'] + except KeyError: + _, _, _, _, total, free, unit_multiplier = cext.linux_sysinfo() + total *= unit_multiplier + free *= unit_multiplier + + used = total - free + percent = usage_percent(used, total, round_=1) + # get pgin/pgouts + try: + f = open_binary("%s/vmstat" % get_procfs_path()) + except IOError as err: + # see https://github.com/giampaolo/psutil/issues/722 + msg = "'sin' and 'sout' swap memory stats couldn't " \ + "be determined and were set to 0 (%s)" % str(err) + warnings.warn(msg, RuntimeWarning) + sin = sout = 0 + else: + with f: + sin = sout = None + for line in f: + # values are expressed in 4 kilo bytes, we want + # bytes instead + if line.startswith(b'pswpin'): + sin = int(line.split(b' ')[1]) * 4 * 1024 + elif line.startswith(b'pswpout'): + sout = int(line.split(b' ')[1]) * 4 * 1024 + if sin is not None and sout is not None: + break + else: + # we might get here when dealing with exotic Linux + # flavors, see: + # https://github.com/giampaolo/psutil/issues/313 + msg = "'sin' and 'sout' swap memory stats couldn't " \ + "be determined and were set to 0" + warnings.warn(msg, RuntimeWarning) + sin = sout = 0 + return _common.sswap(total, used, free, percent, sin, sout) + + +# ===================================================================== +# --- CPU +# ===================================================================== + + +def cpu_times(): + """Return a named tuple representing the following system-wide + CPU times: + (user, nice, system, idle, iowait, irq, softirq [steal, [guest, + [guest_nice]]]) + Last 3 fields may not be available on all Linux kernel versions. + """ + procfs_path = get_procfs_path() + set_scputimes_ntuple(procfs_path) + with open_binary('%s/stat' % procfs_path) as f: + values = f.readline().split() + fields = values[1:len(scputimes._fields) + 1] + fields = [float(x) / CLOCK_TICKS for x in fields] + return scputimes(*fields) + + +def per_cpu_times(): + """Return a list of namedtuple representing the CPU times + for every CPU available on the system. + """ + procfs_path = get_procfs_path() + set_scputimes_ntuple(procfs_path) + cpus = [] + with open_binary('%s/stat' % procfs_path) as f: + # get rid of the first line which refers to system wide CPU stats + f.readline() + for line in f: + if line.startswith(b'cpu'): + values = line.split() + fields = values[1:len(scputimes._fields) + 1] + fields = [float(x) / CLOCK_TICKS for x in fields] + entry = scputimes(*fields) + cpus.append(entry) + return cpus + + +def cpu_count_logical(): + """Return the number of logical CPUs in the system.""" + try: + return os.sysconf("SC_NPROCESSORS_ONLN") + except ValueError: + # as a second fallback we try to parse /proc/cpuinfo + num = 0 + with open_binary('%s/cpuinfo' % get_procfs_path()) as f: + for line in f: + if line.lower().startswith(b'processor'): + num += 1 + + # unknown format (e.g. amrel/sparc architectures), see: + # https://github.com/giampaolo/psutil/issues/200 + # try to parse /proc/stat as a last resort + if num == 0: + search = re.compile(r'cpu\d') + with open_text('%s/stat' % get_procfs_path()) as f: + for line in f: + line = line.split(' ')[0] + if search.match(line): + num += 1 + + if num == 0: + # mimic os.cpu_count() + return None + return num + + +def cpu_count_physical(): + """Return the number of physical cores in the system.""" + # Method #1 + core_ids = set() + for path in glob.glob( + "/sys/devices/system/cpu/cpu[0-9]*/topology/core_id"): + with open_binary(path) as f: + core_ids.add(int(f.read())) + result = len(core_ids) + if result != 0: + return result + + # Method #2 + mapping = {} + current_info = {} + with open_binary('%s/cpuinfo' % get_procfs_path()) as f: + for line in f: + line = line.strip().lower() + if not line: + # new section + if (b'physical id' in current_info and + b'cpu cores' in current_info): + mapping[current_info[b'physical id']] = \ + current_info[b'cpu cores'] + current_info = {} + else: + # ongoing section + if (line.startswith(b'physical id') or + line.startswith(b'cpu cores')): + key, value = line.split(b'\t:', 1) + current_info[key] = int(value) + + result = sum(mapping.values()) + return result or None # mimic os.cpu_count() + + +def cpu_stats(): + """Return various CPU stats as a named tuple.""" + with open_binary('%s/stat' % get_procfs_path()) as f: + ctx_switches = None + interrupts = None + soft_interrupts = None + for line in f: + if line.startswith(b'ctxt'): + ctx_switches = int(line.split()[1]) + elif line.startswith(b'intr'): + interrupts = int(line.split()[1]) + elif line.startswith(b'softirq'): + soft_interrupts = int(line.split()[1]) + if ctx_switches is not None and soft_interrupts is not None \ + and interrupts is not None: + break + syscalls = 0 + return _common.scpustats( + ctx_switches, interrupts, soft_interrupts, syscalls) + + +if os.path.exists("/sys/devices/system/cpu/cpufreq/policy0") or \ + os.path.exists("/sys/devices/system/cpu/cpu0/cpufreq"): + def cpu_freq(): + """Return frequency metrics for all CPUs. + Contrarily to other OSes, Linux updates these values in + real-time. + """ + def get_path(num): + for p in ("/sys/devices/system/cpu/cpufreq/policy%s" % num, + "/sys/devices/system/cpu/cpu%s/cpufreq" % num): + if os.path.exists(p): + return p + + ret = [] + for n in range(cpu_count_logical()): + path = get_path(n) + if not path: + continue + + pjoin = os.path.join + curr = cat(pjoin(path, "scaling_cur_freq"), fallback=None) + if curr is None: + # Likely an old RedHat, see: + # https://github.com/giampaolo/psutil/issues/1071 + curr = cat(pjoin(path, "cpuinfo_cur_freq"), fallback=None) + if curr is None: + raise NotImplementedError( + "can't find current frequency file") + curr = int(curr) / 1000 + max_ = int(cat(pjoin(path, "scaling_max_freq"))) / 1000 + min_ = int(cat(pjoin(path, "scaling_min_freq"))) / 1000 + ret.append(_common.scpufreq(curr, min_, max_)) + return ret + +elif os.path.exists("/proc/cpuinfo"): + def cpu_freq(): + """Alternate implementation using /proc/cpuinfo. + min and max frequencies are not available and are set to None. + """ + ret = [] + with open_binary('%s/cpuinfo' % get_procfs_path()) as f: + for line in f: + if line.lower().startswith(b'cpu mhz'): + key, value = line.split(b'\t:', 1) + ret.append(_common.scpufreq(float(value), 0., 0.)) + return ret + +else: + def cpu_freq(): + """Dummy implementation when none of the above files are present. + """ + return [] + + +# ===================================================================== +# --- network +# ===================================================================== + + +net_if_addrs = cext_posix.net_if_addrs + + +class _Ipv6UnsupportedError(Exception): + pass + + +class Connections: + """A wrapper on top of /proc/net/* files, retrieving per-process + and system-wide open connections (TCP, UDP, UNIX) similarly to + "netstat -an". + + Note: in case of UNIX sockets we're only able to determine the + local endpoint/path, not the one it's connected to. + According to [1] it would be possible but not easily. + + [1] http://serverfault.com/a/417946 + """ + + def __init__(self): + tcp4 = ("tcp", socket.AF_INET, socket.SOCK_STREAM) + tcp6 = ("tcp6", socket.AF_INET6, socket.SOCK_STREAM) + udp4 = ("udp", socket.AF_INET, socket.SOCK_DGRAM) + udp6 = ("udp6", socket.AF_INET6, socket.SOCK_DGRAM) + unix = ("unix", socket.AF_UNIX, None) + self.tmap = { + "all": (tcp4, tcp6, udp4, udp6, unix), + "tcp": (tcp4, tcp6), + "tcp4": (tcp4,), + "tcp6": (tcp6,), + "udp": (udp4, udp6), + "udp4": (udp4,), + "udp6": (udp6,), + "unix": (unix,), + "inet": (tcp4, tcp6, udp4, udp6), + "inet4": (tcp4, udp4), + "inet6": (tcp6, udp6), + } + self._procfs_path = None + + def get_proc_inodes(self, pid): + inodes = defaultdict(list) + for fd in os.listdir("%s/%s/fd" % (self._procfs_path, pid)): + try: + inode = readlink("%s/%s/fd/%s" % (self._procfs_path, pid, fd)) + except (FileNotFoundError, ProcessLookupError): + # ENOENT == file which is gone in the meantime; + # os.stat('/proc/%s' % self.pid) will be done later + # to force NSP (if it's the case) + continue + except OSError as err: + if err.errno == errno.EINVAL: + # not a link + continue + raise + else: + if inode.startswith('socket:['): + # the process is using a socket + inode = inode[8:][:-1] + inodes[inode].append((pid, int(fd))) + return inodes + + def get_all_inodes(self): + inodes = {} + for pid in pids(): + try: + inodes.update(self.get_proc_inodes(pid)) + except (FileNotFoundError, ProcessLookupError, PermissionError): + # os.listdir() is gonna raise a lot of access denied + # exceptions in case of unprivileged user; that's fine + # as we'll just end up returning a connection with PID + # and fd set to None anyway. + # Both netstat -an and lsof does the same so it's + # unlikely we can do any better. + # ENOENT just means a PID disappeared on us. + continue + return inodes + + @staticmethod + def decode_address(addr, family): + """Accept an "ip:port" address as displayed in /proc/net/* + and convert it into a human readable form, like: + + "0500000A:0016" -> ("10.0.0.5", 22) + "0000000000000000FFFF00000100007F:9E49" -> ("::ffff:127.0.0.1", 40521) + + The IP address portion is a little or big endian four-byte + hexadecimal number; that is, the least significant byte is listed + first, so we need to reverse the order of the bytes to convert it + to an IP address. + The port is represented as a two-byte hexadecimal number. + + Reference: + http://linuxdevcenter.com/pub/a/linux/2000/11/16/LinuxAdmin.html + """ + ip, port = addr.split(':') + port = int(port, 16) + # this usually refers to a local socket in listen mode with + # no end-points connected + if not port: + return () + if PY3: + ip = ip.encode('ascii') + if family == socket.AF_INET: + # see: https://github.com/giampaolo/psutil/issues/201 + if LITTLE_ENDIAN: + ip = socket.inet_ntop(family, base64.b16decode(ip)[::-1]) + else: + ip = socket.inet_ntop(family, base64.b16decode(ip)) + else: # IPv6 + # old version - let's keep it, just in case... + # ip = ip.decode('hex') + # return socket.inet_ntop(socket.AF_INET6, + # ''.join(ip[i:i+4][::-1] for i in xrange(0, 16, 4))) + ip = base64.b16decode(ip) + try: + # see: https://github.com/giampaolo/psutil/issues/201 + if LITTLE_ENDIAN: + ip = socket.inet_ntop( + socket.AF_INET6, + struct.pack('>4I', *struct.unpack('<4I', ip))) + else: + ip = socket.inet_ntop( + socket.AF_INET6, + struct.pack('<4I', *struct.unpack('<4I', ip))) + except ValueError: + # see: https://github.com/giampaolo/psutil/issues/623 + if not supports_ipv6(): + raise _Ipv6UnsupportedError + else: + raise + return _common.addr(ip, port) + + @staticmethod + def process_inet(file, family, type_, inodes, filter_pid=None): + """Parse /proc/net/tcp* and /proc/net/udp* files.""" + if file.endswith('6') and not os.path.exists(file): + # IPv6 not supported + return + with open_text(file, buffering=BIGFILE_BUFFERING) as f: + f.readline() # skip the first line + for lineno, line in enumerate(f, 1): + try: + _, laddr, raddr, status, _, _, _, _, _, inode = \ + line.split()[:10] + except ValueError: + raise RuntimeError( + "error while parsing %s; malformed line %s %r" % ( + file, lineno, line)) + if inode in inodes: + # # We assume inet sockets are unique, so we error + # # out if there are multiple references to the + # # same inode. We won't do this for UNIX sockets. + # if len(inodes[inode]) > 1 and family != socket.AF_UNIX: + # raise ValueError("ambiguos inode with multiple " + # "PIDs references") + pid, fd = inodes[inode][0] + else: + pid, fd = None, -1 + if filter_pid is not None and filter_pid != pid: + continue + else: + if type_ == socket.SOCK_STREAM: + status = TCP_STATUSES[status] + else: + status = _common.CONN_NONE + try: + laddr = Connections.decode_address(laddr, family) + raddr = Connections.decode_address(raddr, family) + except _Ipv6UnsupportedError: + continue + yield (fd, family, type_, laddr, raddr, status, pid) + + @staticmethod + def process_unix(file, family, inodes, filter_pid=None): + """Parse /proc/net/unix files.""" + with open_text(file, buffering=BIGFILE_BUFFERING) as f: + f.readline() # skip the first line + for line in f: + tokens = line.split() + try: + _, _, _, _, type_, _, inode = tokens[0:7] + except ValueError: + if ' ' not in line: + # see: https://github.com/giampaolo/psutil/issues/766 + continue + raise RuntimeError( + "error while parsing %s; malformed line %r" % ( + file, line)) + if inode in inodes: + # With UNIX sockets we can have a single inode + # referencing many file descriptors. + pairs = inodes[inode] + else: + pairs = [(None, -1)] + for pid, fd in pairs: + if filter_pid is not None and filter_pid != pid: + continue + else: + if len(tokens) == 8: + path = tokens[-1] + else: + path = "" + type_ = _common.socktype_to_enum(int(type_)) + # XXX: determining the remote endpoint of a + # UNIX socket on Linux is not possible, see: + # https://serverfault.com/questions/252723/ + raddr = "" + status = _common.CONN_NONE + yield (fd, family, type_, path, raddr, status, pid) + + def retrieve(self, kind, pid=None): + if kind not in self.tmap: + raise ValueError("invalid %r kind argument; choose between %s" + % (kind, ', '.join([repr(x) for x in self.tmap]))) + self._procfs_path = get_procfs_path() + if pid is not None: + inodes = self.get_proc_inodes(pid) + if not inodes: + # no connections for this process + return [] + else: + inodes = self.get_all_inodes() + ret = set() + for f, family, type_ in self.tmap[kind]: + if family in (socket.AF_INET, socket.AF_INET6): + ls = self.process_inet( + "%s/net/%s" % (self._procfs_path, f), + family, type_, inodes, filter_pid=pid) + else: + ls = self.process_unix( + "%s/net/%s" % (self._procfs_path, f), + family, inodes, filter_pid=pid) + for fd, family, type_, laddr, raddr, status, bound_pid in ls: + if pid: + conn = _common.pconn(fd, family, type_, laddr, raddr, + status) + else: + conn = _common.sconn(fd, family, type_, laddr, raddr, + status, bound_pid) + ret.add(conn) + return list(ret) + + +_connections = Connections() + + +def net_connections(kind='inet'): + """Return system-wide open connections.""" + return _connections.retrieve(kind) + + +def net_io_counters(): + """Return network I/O statistics for every network interface + installed on the system as a dict of raw tuples. + """ + with open_text("%s/net/dev" % get_procfs_path()) as f: + lines = f.readlines() + retdict = {} + for line in lines[2:]: + colon = line.rfind(':') + assert colon > 0, repr(line) + name = line[:colon].strip() + fields = line[colon + 1:].strip().split() + + # in + (bytes_recv, + packets_recv, + errin, + dropin, + fifoin, # unused + framein, # unused + compressedin, # unused + multicastin, # unused + # out + bytes_sent, + packets_sent, + errout, + dropout, + fifoout, # unused + collisionsout, # unused + carrierout, # unused + compressedout) = map(int, fields) + + retdict[name] = (bytes_sent, bytes_recv, packets_sent, packets_recv, + errin, errout, dropin, dropout) + return retdict + + +def net_if_stats(): + """Get NIC stats (isup, duplex, speed, mtu).""" + duplex_map = {cext.DUPLEX_FULL: NIC_DUPLEX_FULL, + cext.DUPLEX_HALF: NIC_DUPLEX_HALF, + cext.DUPLEX_UNKNOWN: NIC_DUPLEX_UNKNOWN} + names = net_io_counters().keys() + ret = {} + for name in names: + try: + mtu = cext_posix.net_if_mtu(name) + isup = cext_posix.net_if_flags(name) + duplex, speed = cext.net_if_duplex_speed(name) + except OSError as err: + # https://github.com/giampaolo/psutil/issues/1279 + if err.errno != errno.ENODEV: + raise + else: + ret[name] = _common.snicstats(isup, duplex_map[duplex], speed, mtu) + return ret + + +# ===================================================================== +# --- disks +# ===================================================================== + + +disk_usage = _psposix.disk_usage + + +def disk_io_counters(perdisk=False): + """Return disk I/O statistics for every disk installed on the + system as a dict of raw tuples. + """ + def read_procfs(): + # OK, this is a bit confusing. The format of /proc/diskstats can + # have 3 variations. + # On Linux 2.4 each line has always 15 fields, e.g.: + # "3 0 8 hda 8 8 8 8 8 8 8 8 8 8 8" + # On Linux 2.6+ each line *usually* has 14 fields, and the disk + # name is in another position, like this: + # "3 0 hda 8 8 8 8 8 8 8 8 8 8 8" + # ...unless (Linux 2.6) the line refers to a partition instead + # of a disk, in which case the line has less fields (7): + # "3 1 hda1 8 8 8 8" + # 4.18+ has 4 fields added: + # "3 0 hda 8 8 8 8 8 8 8 8 8 8 8 0 0 0 0" + # See: + # https://www.kernel.org/doc/Documentation/iostats.txt + # https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats + with open_text("%s/diskstats" % get_procfs_path()) as f: + lines = f.readlines() + for line in lines: + fields = line.split() + flen = len(fields) + if flen == 15: + # Linux 2.4 + name = fields[3] + reads = int(fields[2]) + (reads_merged, rbytes, rtime, writes, writes_merged, + wbytes, wtime, _, busy_time, _) = map(int, fields[4:14]) + elif flen == 14 or flen == 18: + # Linux 2.6+, line referring to a disk + name = fields[2] + (reads, reads_merged, rbytes, rtime, writes, writes_merged, + wbytes, wtime, _, busy_time, _) = map(int, fields[3:14]) + elif flen == 7: + # Linux 2.6+, line referring to a partition + name = fields[2] + reads, rbytes, writes, wbytes = map(int, fields[3:]) + rtime = wtime = reads_merged = writes_merged = busy_time = 0 + else: + raise ValueError("not sure how to interpret line %r" % line) + yield (name, reads, writes, rbytes, wbytes, rtime, wtime, + reads_merged, writes_merged, busy_time) + + def read_sysfs(): + for block in os.listdir('/sys/block'): + for root, _, files in os.walk(os.path.join('/sys/block', block)): + if 'stat' not in files: + continue + with open_text(os.path.join(root, 'stat')) as f: + fields = f.read().strip().split() + name = os.path.basename(root) + (reads, reads_merged, rbytes, rtime, writes, writes_merged, + wbytes, wtime, _, busy_time, _) = map(int, fields) + yield (name, reads, writes, rbytes, wbytes, rtime, + wtime, reads_merged, writes_merged, busy_time) + + if os.path.exists('%s/diskstats' % get_procfs_path()): + gen = read_procfs() + elif os.path.exists('/sys/block'): + gen = read_sysfs() + else: + raise NotImplementedError( + "%s/diskstats nor /sys/block filesystem are available on this " + "system" % get_procfs_path()) + + retdict = {} + for entry in gen: + (name, reads, writes, rbytes, wbytes, rtime, wtime, reads_merged, + writes_merged, busy_time) = entry + if not perdisk and not is_storage_device(name): + # perdisk=False means we want to calculate totals so we skip + # partitions (e.g. 'sda1', 'nvme0n1p1') and only include + # base disk devices (e.g. 'sda', 'nvme0n1'). Base disks + # include a total of all their partitions + some extra size + # of their own: + # $ cat /proc/diskstats + # 259 0 sda 10485760 ... + # 259 1 sda1 5186039 ... + # 259 1 sda2 5082039 ... + # See: + # https://github.com/giampaolo/psutil/pull/1313 + continue + + rbytes *= DISK_SECTOR_SIZE + wbytes *= DISK_SECTOR_SIZE + retdict[name] = (reads, writes, rbytes, wbytes, rtime, wtime, + reads_merged, writes_merged, busy_time) + + return retdict + + +def disk_partitions(all=False): + """Return mounted disk partitions as a list of namedtuples.""" + fstypes = set() + procfs_path = get_procfs_path() + with open_text("%s/filesystems" % procfs_path) as f: + for line in f: + line = line.strip() + if not line.startswith("nodev"): + fstypes.add(line.strip()) + else: + # ignore all lines starting with "nodev" except "nodev zfs" + fstype = line.split("\t")[1] + if fstype == "zfs": + fstypes.add("zfs") + + # See: https://github.com/giampaolo/psutil/issues/1307 + if procfs_path == "/proc" and os.path.isfile('/etc/mtab'): + mounts_path = os.path.realpath("/etc/mtab") + else: + mounts_path = os.path.realpath("%s/self/mounts" % procfs_path) + + retlist = [] + partitions = cext.disk_partitions(mounts_path) + for partition in partitions: + device, mountpoint, fstype, opts = partition + if device == 'none': + device = '' + if not all: + if device == '' or fstype not in fstypes: + continue + ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + retlist.append(ntuple) + return retlist + + +# ===================================================================== +# --- sensors +# ===================================================================== + + +def sensors_temperatures(): + """Return hardware (CPU and others) temperatures as a dict + including hardware name, label, current, max and critical + temperatures. + + Implementation notes: + - /sys/class/hwmon looks like the most recent interface to + retrieve this info, and this implementation relies on it + only (old distros will probably use something else) + - lm-sensors on Ubuntu 16.04 relies on /sys/class/hwmon + - /sys/class/thermal/thermal_zone* is another one but it's more + difficult to parse + """ + ret = collections.defaultdict(list) + basenames = glob.glob('/sys/class/hwmon/hwmon*/temp*_*') + # CentOS has an intermediate /device directory: + # https://github.com/giampaolo/psutil/issues/971 + # https://github.com/nicolargo/glances/issues/1060 + basenames.extend(glob.glob('/sys/class/hwmon/hwmon*/device/temp*_*')) + basenames = sorted(set([x.split('_')[0] for x in basenames])) + + for base in basenames: + try: + path = base + '_input' + current = float(cat(path)) / 1000.0 + path = os.path.join(os.path.dirname(base), 'name') + unit_name = cat(path, binary=False) + except (IOError, OSError, ValueError) as err: + # A lot of things can go wrong here, so let's just skip the + # whole entry. Sure thing is Linux's /sys/class/hwmon really + # is a stinky broken mess. + # https://github.com/giampaolo/psutil/issues/1009 + # https://github.com/giampaolo/psutil/issues/1101 + # https://github.com/giampaolo/psutil/issues/1129 + # https://github.com/giampaolo/psutil/issues/1245 + # https://github.com/giampaolo/psutil/issues/1323 + warnings.warn("ignoring %r for file %r" % (err, path), + RuntimeWarning) + continue + + high = cat(base + '_max', fallback=None) + critical = cat(base + '_crit', fallback=None) + label = cat(base + '_label', fallback='', binary=False) + + if high is not None: + try: + high = float(high) / 1000.0 + except ValueError: + high = None + if critical is not None: + try: + critical = float(critical) / 1000.0 + except ValueError: + critical = None + + ret[unit_name].append((label, current, high, critical)) + + # Indication that no sensors were detected in /sys/class/hwmon/ + if not basenames: + basenames = glob.glob('/sys/class/thermal/thermal_zone*') + basenames = sorted(set(basenames)) + + for base in basenames: + try: + path = os.path.join(base, 'temp') + current = float(cat(path)) / 1000.0 + path = os.path.join(base, 'type') + unit_name = cat(path, binary=False) + except (IOError, OSError, ValueError) as err: + warnings.warn("ignoring %r for file %r" % (err, path), + RuntimeWarning) + continue + + trip_paths = glob.glob(base + '/trip_point*') + trip_points = set(['_'.join( + os.path.basename(p).split('_')[0:3]) for p in trip_paths]) + critical = None + high = None + for trip_point in trip_points: + path = os.path.join(base, trip_point + "_type") + trip_type = cat(path, fallback='', binary=False) + if trip_type == 'critical': + critical = cat(os.path.join(base, trip_point + "_temp"), + fallback=None) + elif trip_type == 'high': + high = cat(os.path.join(base, trip_point + "_temp"), + fallback=None) + + if high is not None: + try: + high = float(high) / 1000.0 + except ValueError: + high = None + if critical is not None: + try: + critical = float(critical) / 1000.0 + except ValueError: + critical = None + + ret[unit_name].append(('', current, high, critical)) + + return dict(ret) + + +def sensors_fans(): + """Return hardware fans info (for CPU and other peripherals) as a + dict including hardware label and current speed. + + Implementation notes: + - /sys/class/hwmon looks like the most recent interface to + retrieve this info, and this implementation relies on it + only (old distros will probably use something else) + - lm-sensors on Ubuntu 16.04 relies on /sys/class/hwmon + """ + ret = collections.defaultdict(list) + basenames = glob.glob('/sys/class/hwmon/hwmon*/fan*_*') + if not basenames: + # CentOS has an intermediate /device directory: + # https://github.com/giampaolo/psutil/issues/971 + basenames = glob.glob('/sys/class/hwmon/hwmon*/device/fan*_*') + + basenames = sorted(set([x.split('_')[0] for x in basenames])) + for base in basenames: + try: + current = int(cat(base + '_input')) + except (IOError, OSError) as err: + warnings.warn("ignoring %r" % err, RuntimeWarning) + continue + unit_name = cat(os.path.join(os.path.dirname(base), 'name'), + binary=False) + label = cat(base + '_label', fallback='', binary=False) + ret[unit_name].append(_common.sfan(label, current)) + + return dict(ret) + + +def sensors_battery(): + """Return battery information. + Implementation note: it appears /sys/class/power_supply/BAT0/ + directory structure may vary and provide files with the same + meaning but under different names, see: + https://github.com/giampaolo/psutil/issues/966 + """ + null = object() + + def multi_cat(*paths): + """Attempt to read the content of multiple files which may + not exist. If none of them exist return None. + """ + for path in paths: + ret = cat(path, fallback=null) + if ret != null: + return int(ret) if ret.isdigit() else ret + return None + + bats = [x for x in os.listdir(POWER_SUPPLY_PATH) if x.startswith('BAT')] + if not bats: + return None + # Get the first available battery. Usually this is "BAT0", except + # some rare exceptions: + # https://github.com/giampaolo/psutil/issues/1238 + root = os.path.join(POWER_SUPPLY_PATH, sorted(bats)[0]) + + # Base metrics. + energy_now = multi_cat( + root + "/energy_now", + root + "/charge_now") + power_now = multi_cat( + root + "/power_now", + root + "/current_now") + energy_full = multi_cat( + root + "/energy_full", + root + "/charge_full") + if energy_now is None or power_now is None: + return None + + # Percent. If we have energy_full the percentage will be more + # accurate compared to reading /capacity file (float vs. int). + if energy_full is not None: + try: + percent = 100.0 * energy_now / energy_full + except ZeroDivisionError: + percent = 0.0 + else: + percent = int(cat(root + "/capacity", fallback=-1)) + if percent == -1: + return None + + # Is AC power cable plugged in? + # Note: AC0 is not always available and sometimes (e.g. CentOS7) + # it's called "AC". + power_plugged = None + online = multi_cat( + os.path.join(POWER_SUPPLY_PATH, "AC0/online"), + os.path.join(POWER_SUPPLY_PATH, "AC/online")) + if online is not None: + power_plugged = online == 1 + else: + status = cat(root + "/status", fallback="", binary=False).lower() + if status == "discharging": + power_plugged = False + elif status in ("charging", "full"): + power_plugged = True + + # Seconds left. + # Note to self: we may also calculate the charging ETA as per: + # https://github.com/thialfihar/dotfiles/blob/ + # 013937745fd9050c30146290e8f963d65c0179e6/bin/battery.py#L55 + if power_plugged: + secsleft = _common.POWER_TIME_UNLIMITED + else: + try: + secsleft = int(energy_now / power_now * 3600) + except ZeroDivisionError: + secsleft = _common.POWER_TIME_UNKNOWN + + return _common.sbattery(percent, secsleft, power_plugged) + + +# ===================================================================== +# --- other system functions +# ===================================================================== + + +def users(): + """Return currently connected users as a list of namedtuples.""" + retlist = [] + rawlist = cext.users() + for item in rawlist: + user, tty, hostname, tstamp, user_process, pid = item + # note: the underlying C function includes entries about + # system boot, run level and others. We might want + # to use them in the future. + if not user_process: + continue + if hostname in (':0.0', ':0'): + hostname = 'localhost' + nt = _common.suser(user, tty or None, hostname, tstamp, pid) + retlist.append(nt) + return retlist + + +def boot_time(): + """Return the system boot time expressed in seconds since the epoch.""" + global BOOT_TIME + path = '%s/stat' % get_procfs_path() + with open_binary(path) as f: + for line in f: + if line.startswith(b'btime'): + ret = float(line.strip().split()[1]) + BOOT_TIME = ret + return ret + raise RuntimeError( + "line 'btime' not found in %s" % path) + + +# ===================================================================== +# --- processes +# ===================================================================== + + +def pids(): + """Returns a list of PIDs currently running on the system.""" + return [int(x) for x in os.listdir(b(get_procfs_path())) if x.isdigit()] + + +def pid_exists(pid): + """Check for the existence of a unix PID. Linux TIDs are not + supported (always return False). + """ + if not _psposix.pid_exists(pid): + return False + else: + # Linux's apparently does not distinguish between PIDs and TIDs + # (thread IDs). + # listdir("/proc") won't show any TID (only PIDs) but + # os.stat("/proc/{tid}") will succeed if {tid} exists. + # os.kill() can also be passed a TID. This is quite confusing. + # In here we want to enforce this distinction and support PIDs + # only, see: + # https://github.com/giampaolo/psutil/issues/687 + try: + # Note: already checked that this is faster than using a + # regular expr. Also (a lot) faster than doing + # 'return pid in pids()' + path = "%s/%s/status" % (get_procfs_path(), pid) + with open_binary(path) as f: + for line in f: + if line.startswith(b"Tgid:"): + tgid = int(line.split()[1]) + # If tgid and pid are the same then we're + # dealing with a process PID. + return tgid == pid + raise ValueError("'Tgid' line not found in %s" % path) + except (EnvironmentError, ValueError): + return pid in pids() + + +def ppid_map(): + """Obtain a {pid: ppid, ...} dict for all running processes in + one shot. Used to speed up Process.children(). + """ + ret = {} + procfs_path = get_procfs_path() + for pid in pids(): + try: + with open_binary("%s/%s/stat" % (procfs_path, pid)) as f: + data = f.read() + except (FileNotFoundError, ProcessLookupError): + # Note: we should be able to access /stat for all processes + # aka it's unlikely we'll bump into EPERM, which is good. + pass + else: + rpar = data.rfind(b')') + dset = data[rpar + 2:].split() + ppid = int(dset[1]) + ret[pid] = ppid + return ret + + +def wrap_exceptions(fun): + """Decorator which translates bare OSError and IOError exceptions + into NoSuchProcess and AccessDenied. + """ + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + try: + return fun(self, *args, **kwargs) + except PermissionError: + raise AccessDenied(self.pid, self._name) + except ProcessLookupError: + raise NoSuchProcess(self.pid, self._name) + except FileNotFoundError: + if not os.path.exists("%s/%s" % (self._procfs_path, self.pid)): + raise NoSuchProcess(self.pid, self._name) + # Note: zombies will keep existing under /proc until they're + # gone so there's no way to distinguish them in here. + raise + return wrapper + + +class Process(object): + """Linux process implementation.""" + + __slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"] + + def __init__(self, pid): + self.pid = pid + self._name = None + self._ppid = None + self._procfs_path = get_procfs_path() + + def _assert_alive(self): + """Raise NSP if the process disappeared on us.""" + # For those C function who do not raise NSP, possibly returning + # incorrect or incomplete result. + os.stat('%s/%s' % (self._procfs_path, self.pid)) + + @wrap_exceptions + @memoize_when_activated + def _parse_stat_file(self): + """Parse /proc/{pid}/stat file and return a dict with various + process info. + Using "man proc" as a reference: where "man proc" refers to + position N always substract 3 (e.g ppid position 4 in + 'man proc' == position 1 in here). + The return value is cached in case oneshot() ctx manager is + in use. + """ + with open_binary("%s/%s/stat" % (self._procfs_path, self.pid)) as f: + data = f.read() + # Process name is between parentheses. It can contain spaces and + # other parentheses. This is taken into account by looking for + # the first occurrence of "(" and the last occurence of ")". + rpar = data.rfind(b')') + name = data[data.find(b'(') + 1:rpar] + fields = data[rpar + 2:].split() + + ret = {} + ret['name'] = name + ret['status'] = fields[0] + ret['ppid'] = fields[1] + ret['ttynr'] = fields[4] + ret['utime'] = fields[11] + ret['stime'] = fields[12] + ret['children_utime'] = fields[13] + ret['children_stime'] = fields[14] + ret['create_time'] = fields[19] + ret['cpu_num'] = fields[36] + ret['blkio_ticks'] = fields[39] # aka 'delayacct_blkio_ticks' + + return ret + + @wrap_exceptions + @memoize_when_activated + def _read_status_file(self): + """Read /proc/{pid}/stat file and return its content. + The return value is cached in case oneshot() ctx manager is + in use. + """ + with open_binary("%s/%s/status" % (self._procfs_path, self.pid)) as f: + return f.read() + + @wrap_exceptions + @memoize_when_activated + def _read_smaps_file(self): + with open_binary("%s/%s/smaps" % (self._procfs_path, self.pid), + buffering=BIGFILE_BUFFERING) as f: + return f.read().strip() + + def oneshot_enter(self): + self._parse_stat_file.cache_activate(self) + self._read_status_file.cache_activate(self) + self._read_smaps_file.cache_activate(self) + + def oneshot_exit(self): + self._parse_stat_file.cache_deactivate(self) + self._read_status_file.cache_deactivate(self) + self._read_smaps_file.cache_deactivate(self) + + @wrap_exceptions + def name(self): + name = self._parse_stat_file()['name'] + if PY3: + name = decode(name) + # XXX - gets changed later and probably needs refactoring + return name + + def exe(self): + try: + return readlink("%s/%s/exe" % (self._procfs_path, self.pid)) + except (FileNotFoundError, ProcessLookupError): + # no such file error; might be raised also if the + # path actually exists for system processes with + # low pids (about 0-20) + if os.path.lexists("%s/%s" % (self._procfs_path, self.pid)): + return "" + else: + if not pid_exists(self.pid): + raise NoSuchProcess(self.pid, self._name) + else: + raise ZombieProcess(self.pid, self._name, self._ppid) + except PermissionError: + raise AccessDenied(self.pid, self._name) + + @wrap_exceptions + def cmdline(self): + with open_text("%s/%s/cmdline" % (self._procfs_path, self.pid)) as f: + data = f.read() + if not data: + # may happen in case of zombie process + return [] + # 'man proc' states that args are separated by null bytes '\0' + # and last char is supposed to be a null byte. Nevertheless + # some processes may change their cmdline after being started + # (via setproctitle() or similar), they are usually not + # compliant with this rule and use spaces instead. Google + # Chrome process is an example. See: + # https://github.com/giampaolo/psutil/issues/1179 + sep = '\x00' if data.endswith('\x00') else ' ' + if data.endswith(sep): + data = data[:-1] + cmdline = data.split(sep) + # Sometimes last char is a null byte '\0' but the args are + # separated by spaces, see: https://github.com/giampaolo/psutil/ + # issues/1179#issuecomment-552984549 + if sep == '\x00' and len(cmdline) == 1 and ' ' in data: + cmdline = data.split(' ') + return cmdline + + @wrap_exceptions + def environ(self): + with open_text("%s/%s/environ" % (self._procfs_path, self.pid)) as f: + data = f.read() + return parse_environ_block(data) + + @wrap_exceptions + def terminal(self): + tty_nr = int(self._parse_stat_file()['ttynr']) + tmap = _psposix.get_terminal_map() + try: + return tmap[tty_nr] + except KeyError: + return None + + # May not be available on old kernels. + if os.path.exists('/proc/%s/io' % os.getpid()): + @wrap_exceptions + def io_counters(self): + fname = "%s/%s/io" % (self._procfs_path, self.pid) + fields = {} + with open_binary(fname) as f: + for line in f: + # https://github.com/giampaolo/psutil/issues/1004 + line = line.strip() + if line: + try: + name, value = line.split(b': ') + except ValueError: + # https://github.com/giampaolo/psutil/issues/1004 + continue + else: + fields[name] = int(value) + if not fields: + raise RuntimeError("%s file was empty" % fname) + try: + return pio( + fields[b'syscr'], # read syscalls + fields[b'syscw'], # write syscalls + fields[b'read_bytes'], # read bytes + fields[b'write_bytes'], # write bytes + fields[b'rchar'], # read chars + fields[b'wchar'], # write chars + ) + except KeyError as err: + raise ValueError("%r field was not found in %s; found fields " + "are %r" % (err[0], fname, fields)) + + @wrap_exceptions + def cpu_times(self): + values = self._parse_stat_file() + utime = float(values['utime']) / CLOCK_TICKS + stime = float(values['stime']) / CLOCK_TICKS + children_utime = float(values['children_utime']) / CLOCK_TICKS + children_stime = float(values['children_stime']) / CLOCK_TICKS + iowait = float(values['blkio_ticks']) / CLOCK_TICKS + return pcputimes(utime, stime, children_utime, children_stime, iowait) + + @wrap_exceptions + def cpu_num(self): + """What CPU the process is on.""" + return int(self._parse_stat_file()['cpu_num']) + + @wrap_exceptions + def wait(self, timeout=None): + return _psposix.wait_pid(self.pid, timeout, self._name) + + @wrap_exceptions + def create_time(self): + ctime = float(self._parse_stat_file()['create_time']) + # According to documentation, starttime is in field 21 and the + # unit is jiffies (clock ticks). + # We first divide it for clock ticks and then add uptime returning + # seconds since the epoch, in UTC. + # Also use cached value if available. + bt = BOOT_TIME or boot_time() + return (ctime / CLOCK_TICKS) + bt + + @wrap_exceptions + def memory_info(self): + # ============================================================ + # | FIELD | DESCRIPTION | AKA | TOP | + # ============================================================ + # | rss | resident set size | | RES | + # | vms | total program size | size | VIRT | + # | shared | shared pages (from shared mappings) | | SHR | + # | text | text ('code') | trs | CODE | + # | lib | library (unused in Linux 2.6) | lrs | | + # | data | data + stack | drs | DATA | + # | dirty | dirty pages (unused in Linux 2.6) | dt | | + # ============================================================ + with open_binary("%s/%s/statm" % (self._procfs_path, self.pid)) as f: + vms, rss, shared, text, lib, data, dirty = \ + [int(x) * PAGESIZE for x in f.readline().split()[:7]] + return pmem(rss, vms, shared, text, lib, data, dirty) + + # /proc/pid/smaps does not exist on kernels < 2.6.14 or if + # CONFIG_MMU kernel configuration option is not enabled. + if HAS_SMAPS: + + @wrap_exceptions + def memory_full_info( + self, + # Gets Private_Clean, Private_Dirty, Private_Hugetlb. + _private_re=re.compile(br"\nPrivate.*:\s+(\d+)"), + _pss_re=re.compile(br"\nPss\:\s+(\d+)"), + _swap_re=re.compile(br"\nSwap\:\s+(\d+)")): + basic_mem = self.memory_info() + # Note: using 3 regexes is faster than reading the file + # line by line. + # XXX: on Python 3 the 2 regexes are 30% slower than on + # Python 2 though. Figure out why. + # + # You might be tempted to calculate USS by subtracting + # the "shared" value from the "resident" value in + # /proc//statm. But at least on Linux, statm's "shared" + # value actually counts pages backed by files, which has + # little to do with whether the pages are actually shared. + # /proc/self/smaps on the other hand appears to give us the + # correct information. + smaps_data = self._read_smaps_file() + # Note: smaps file can be empty for certain processes. + # The code below will not crash though and will result to 0. + uss = sum(map(int, _private_re.findall(smaps_data))) * 1024 + pss = sum(map(int, _pss_re.findall(smaps_data))) * 1024 + swap = sum(map(int, _swap_re.findall(smaps_data))) * 1024 + return pfullmem(*basic_mem + (uss, pss, swap)) + + else: + memory_full_info = memory_info + + if HAS_SMAPS: + + @wrap_exceptions + def memory_maps(self): + """Return process's mapped memory regions as a list of named + tuples. Fields are explained in 'man proc'; here is an updated + (Apr 2012) version: http://goo.gl/fmebo + + /proc/{PID}/smaps does not exist on kernels < 2.6.14 or if + CONFIG_MMU kernel configuration option is not enabled. + """ + def get_blocks(lines, current_block): + data = {} + for line in lines: + fields = line.split(None, 5) + if not fields[0].endswith(b':'): + # new block section + yield (current_block.pop(), data) + current_block.append(line) + else: + try: + data[fields[0]] = int(fields[1]) * 1024 + except ValueError: + if fields[0].startswith(b'VmFlags:'): + # see issue #369 + continue + else: + raise ValueError("don't know how to inte" + "rpret line %r" % line) + yield (current_block.pop(), data) + + data = self._read_smaps_file() + # Note: smaps file can be empty for certain processes. + if not data: + return [] + lines = data.split(b'\n') + ls = [] + first_line = lines.pop(0) + current_block = [first_line] + for header, data in get_blocks(lines, current_block): + hfields = header.split(None, 5) + try: + addr, perms, offset, dev, inode, path = hfields + except ValueError: + addr, perms, offset, dev, inode, path = \ + hfields + [''] + if not path: + path = '[anon]' + else: + if PY3: + path = decode(path) + path = path.strip() + if (path.endswith(' (deleted)') and not + path_exists_strict(path)): + path = path[:-10] + ls.append(( + decode(addr), decode(perms), path, + data[b'Rss:'], + data.get(b'Size:', 0), + data.get(b'Pss:', 0), + data.get(b'Shared_Clean:', 0), + data.get(b'Shared_Dirty:', 0), + data.get(b'Private_Clean:', 0), + data.get(b'Private_Dirty:', 0), + data.get(b'Referenced:', 0), + data.get(b'Anonymous:', 0), + data.get(b'Swap:', 0) + )) + return ls + + @wrap_exceptions + def cwd(self): + try: + return readlink("%s/%s/cwd" % (self._procfs_path, self.pid)) + except (FileNotFoundError, ProcessLookupError): + # https://github.com/giampaolo/psutil/issues/986 + if not pid_exists(self.pid): + raise NoSuchProcess(self.pid, self._name) + else: + raise ZombieProcess(self.pid, self._name, self._ppid) + + @wrap_exceptions + def num_ctx_switches(self, + _ctxsw_re=re.compile(br'ctxt_switches:\t(\d+)')): + data = self._read_status_file() + ctxsw = _ctxsw_re.findall(data) + if not ctxsw: + raise NotImplementedError( + "'voluntary_ctxt_switches' and 'nonvoluntary_ctxt_switches'" + "lines were not found in %s/%s/status; the kernel is " + "probably older than 2.6.23" % ( + self._procfs_path, self.pid)) + else: + return _common.pctxsw(int(ctxsw[0]), int(ctxsw[1])) + + @wrap_exceptions + def num_threads(self, _num_threads_re=re.compile(br'Threads:\t(\d+)')): + # Note: on Python 3 using a re is faster than iterating over file + # line by line. On Python 2 is the exact opposite, and iterating + # over a file on Python 3 is slower than on Python 2. + data = self._read_status_file() + return int(_num_threads_re.findall(data)[0]) + + @wrap_exceptions + def threads(self): + thread_ids = os.listdir("%s/%s/task" % (self._procfs_path, self.pid)) + thread_ids.sort() + retlist = [] + hit_enoent = False + for thread_id in thread_ids: + fname = "%s/%s/task/%s/stat" % ( + self._procfs_path, self.pid, thread_id) + try: + with open_binary(fname) as f: + st = f.read().strip() + except FileNotFoundError: + # no such file or directory; it means thread + # disappeared on us + hit_enoent = True + continue + # ignore the first two values ("pid (exe)") + st = st[st.find(b')') + 2:] + values = st.split(b' ') + utime = float(values[11]) / CLOCK_TICKS + stime = float(values[12]) / CLOCK_TICKS + ntuple = _common.pthread(int(thread_id), utime, stime) + retlist.append(ntuple) + if hit_enoent: + self._assert_alive() + return retlist + + @wrap_exceptions + def nice_get(self): + # with open_text('%s/%s/stat' % (self._procfs_path, self.pid)) as f: + # data = f.read() + # return int(data.split()[18]) + + # Use C implementation + return cext_posix.getpriority(self.pid) + + @wrap_exceptions + def nice_set(self, value): + return cext_posix.setpriority(self.pid, value) + + # starting from CentOS 6. + if HAS_CPU_AFFINITY: + + @wrap_exceptions + def cpu_affinity_get(self): + return cext.proc_cpu_affinity_get(self.pid) + + def _get_eligible_cpus( + self, _re=re.compile(br"Cpus_allowed_list:\t(\d+)-(\d+)")): + # See: https://github.com/giampaolo/psutil/issues/956 + data = self._read_status_file() + match = _re.findall(data) + if match: + return list(range(int(match[0][0]), int(match[0][1]) + 1)) + else: + return list(range(len(per_cpu_times()))) + + @wrap_exceptions + def cpu_affinity_set(self, cpus): + try: + cext.proc_cpu_affinity_set(self.pid, cpus) + except (OSError, ValueError) as err: + if isinstance(err, ValueError) or err.errno == errno.EINVAL: + eligible_cpus = self._get_eligible_cpus() + all_cpus = tuple(range(len(per_cpu_times()))) + for cpu in cpus: + if cpu not in all_cpus: + raise ValueError( + "invalid CPU number %r; choose between %s" % ( + cpu, eligible_cpus)) + if cpu not in eligible_cpus: + raise ValueError( + "CPU number %r is not eligible; choose " + "between %s" % (cpu, eligible_cpus)) + raise + + # only starting from kernel 2.6.13 + if HAS_PROC_IO_PRIORITY: + + @wrap_exceptions + def ionice_get(self): + ioclass, value = cext.proc_ioprio_get(self.pid) + if enum is not None: + ioclass = IOPriority(ioclass) + return _common.pionice(ioclass, value) + + @wrap_exceptions + def ionice_set(self, ioclass, value): + if value is None: + value = 0 + if value and ioclass in (IOPRIO_CLASS_IDLE, IOPRIO_CLASS_NONE): + raise ValueError("%r ioclass accepts no value" % ioclass) + if value < 0 or value > 7: + raise ValueError("value not in 0-7 range") + return cext.proc_ioprio_set(self.pid, ioclass, value) + + if HAS_PRLIMIT: + + @wrap_exceptions + def rlimit(self, resource, limits=None): + # If pid is 0 prlimit() applies to the calling process and + # we don't want that. We should never get here though as + # PID 0 is not supported on Linux. + if self.pid == 0: + raise ValueError("can't use prlimit() against PID 0 process") + try: + if limits is None: + # get + return cext.linux_prlimit(self.pid, resource) + else: + # set + if len(limits) != 2: + raise ValueError( + "second argument must be a (soft, hard) tuple, " + "got %s" % repr(limits)) + soft, hard = limits + cext.linux_prlimit(self.pid, resource, soft, hard) + except OSError as err: + if err.errno == errno.ENOSYS and pid_exists(self.pid): + # I saw this happening on Travis: + # https://travis-ci.org/giampaolo/psutil/jobs/51368273 + raise ZombieProcess(self.pid, self._name, self._ppid) + else: + raise + + @wrap_exceptions + def status(self): + letter = self._parse_stat_file()['status'] + if PY3: + letter = letter.decode() + # XXX is '?' legit? (we're not supposed to return it anyway) + return PROC_STATUSES.get(letter, '?') + + @wrap_exceptions + def open_files(self): + retlist = [] + files = os.listdir("%s/%s/fd" % (self._procfs_path, self.pid)) + hit_enoent = False + for fd in files: + file = "%s/%s/fd/%s" % (self._procfs_path, self.pid, fd) + try: + path = readlink(file) + except (FileNotFoundError, ProcessLookupError): + # ENOENT == file which is gone in the meantime + hit_enoent = True + continue + except OSError as err: + if err.errno == errno.EINVAL: + # not a link + continue + raise + else: + # If path is not an absolute there's no way to tell + # whether it's a regular file or not, so we skip it. + # A regular file is always supposed to be have an + # absolute path though. + if path.startswith('/') and isfile_strict(path): + # Get file position and flags. + file = "%s/%s/fdinfo/%s" % ( + self._procfs_path, self.pid, fd) + try: + with open_binary(file) as f: + pos = int(f.readline().split()[1]) + flags = int(f.readline().split()[1], 8) + except FileNotFoundError: + # fd gone in the meantime; process may + # still be alive + hit_enoent = True + else: + mode = file_flags_to_mode(flags) + ntuple = popenfile( + path, int(fd), int(pos), mode, flags) + retlist.append(ntuple) + if hit_enoent: + self._assert_alive() + return retlist + + @wrap_exceptions + def connections(self, kind='inet'): + ret = _connections.retrieve(kind, self.pid) + self._assert_alive() + return ret + + @wrap_exceptions + def num_fds(self): + return len(os.listdir("%s/%s/fd" % (self._procfs_path, self.pid))) + + @wrap_exceptions + def ppid(self): + return int(self._parse_stat_file()['ppid']) + + @wrap_exceptions + def uids(self, _uids_re=re.compile(br'Uid:\t(\d+)\t(\d+)\t(\d+)')): + data = self._read_status_file() + real, effective, saved = _uids_re.findall(data)[0] + return _common.puids(int(real), int(effective), int(saved)) + + @wrap_exceptions + def gids(self, _gids_re=re.compile(br'Gid:\t(\d+)\t(\d+)\t(\d+)')): + data = self._read_status_file() + real, effective, saved = _gids_re.findall(data)[0] + return _common.pgids(int(real), int(effective), int(saved)) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psosx.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psosx.py new file mode 100644 index 000000000..7f28447bb --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psosx.py @@ -0,0 +1,568 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""macOS platform implementation.""" + +import contextlib +import errno +import functools +import os +from collections import namedtuple + +from . import _common +from . import _psposix +from . import _psutil_osx as cext +from . import _psutil_posix as cext_posix +from ._common import conn_tmap +from ._common import conn_to_ntuple +from ._common import isfile_strict +from ._common import memoize_when_activated +from ._common import parse_environ_block +from ._compat import PermissionError +from ._compat import ProcessLookupError +from ._common import usage_percent + + +__extra__all__ = [] + + +# ===================================================================== +# --- globals +# ===================================================================== + + +PAGESIZE = os.sysconf("SC_PAGE_SIZE") +AF_LINK = cext_posix.AF_LINK + +TCP_STATUSES = { + cext.TCPS_ESTABLISHED: _common.CONN_ESTABLISHED, + cext.TCPS_SYN_SENT: _common.CONN_SYN_SENT, + cext.TCPS_SYN_RECEIVED: _common.CONN_SYN_RECV, + cext.TCPS_FIN_WAIT_1: _common.CONN_FIN_WAIT1, + cext.TCPS_FIN_WAIT_2: _common.CONN_FIN_WAIT2, + cext.TCPS_TIME_WAIT: _common.CONN_TIME_WAIT, + cext.TCPS_CLOSED: _common.CONN_CLOSE, + cext.TCPS_CLOSE_WAIT: _common.CONN_CLOSE_WAIT, + cext.TCPS_LAST_ACK: _common.CONN_LAST_ACK, + cext.TCPS_LISTEN: _common.CONN_LISTEN, + cext.TCPS_CLOSING: _common.CONN_CLOSING, + cext.PSUTIL_CONN_NONE: _common.CONN_NONE, +} + +PROC_STATUSES = { + cext.SIDL: _common.STATUS_IDLE, + cext.SRUN: _common.STATUS_RUNNING, + cext.SSLEEP: _common.STATUS_SLEEPING, + cext.SSTOP: _common.STATUS_STOPPED, + cext.SZOMB: _common.STATUS_ZOMBIE, +} + +kinfo_proc_map = dict( + ppid=0, + ruid=1, + euid=2, + suid=3, + rgid=4, + egid=5, + sgid=6, + ttynr=7, + ctime=8, + status=9, + name=10, +) + +pidtaskinfo_map = dict( + cpuutime=0, + cpustime=1, + rss=2, + vms=3, + pfaults=4, + pageins=5, + numthreads=6, + volctxsw=7, +) + +# These objects get set on "import psutil" from the __init__.py +# file, see: https://github.com/giampaolo/psutil/issues/1402 +NoSuchProcess = None +ZombieProcess = None +AccessDenied = None +TimeoutExpired = None + + +# ===================================================================== +# --- named tuples +# ===================================================================== + + +# psutil.cpu_times() +scputimes = namedtuple('scputimes', ['user', 'nice', 'system', 'idle']) +# psutil.virtual_memory() +svmem = namedtuple( + 'svmem', ['total', 'available', 'percent', 'used', 'free', + 'active', 'inactive', 'wired']) +# psutil.Process.memory_info() +pmem = namedtuple('pmem', ['rss', 'vms', 'pfaults', 'pageins']) +# psutil.Process.memory_full_info() +pfullmem = namedtuple('pfullmem', pmem._fields + ('uss', )) + + +# ===================================================================== +# --- memory +# ===================================================================== + + +def virtual_memory(): + """System virtual memory as a namedtuple.""" + total, active, inactive, wired, free, speculative = cext.virtual_mem() + # This is how Zabbix calculate avail and used mem: + # https://github.com/zabbix/zabbix/blob/trunk/src/libs/zbxsysinfo/ + # osx/memory.c + # Also see: https://github.com/giampaolo/psutil/issues/1277 + avail = inactive + free + used = active + wired + # This is NOT how Zabbix calculates free mem but it matches "free" + # cmdline utility. + free -= speculative + percent = usage_percent((total - avail), total, round_=1) + return svmem(total, avail, percent, used, free, + active, inactive, wired) + + +def swap_memory(): + """Swap system memory as a (total, used, free, sin, sout) tuple.""" + total, used, free, sin, sout = cext.swap_mem() + percent = usage_percent(used, total, round_=1) + return _common.sswap(total, used, free, percent, sin, sout) + + +# ===================================================================== +# --- CPU +# ===================================================================== + + +def cpu_times(): + """Return system CPU times as a namedtuple.""" + user, nice, system, idle = cext.cpu_times() + return scputimes(user, nice, system, idle) + + +def per_cpu_times(): + """Return system CPU times as a named tuple""" + ret = [] + for cpu_t in cext.per_cpu_times(): + user, nice, system, idle = cpu_t + item = scputimes(user, nice, system, idle) + ret.append(item) + return ret + + +def cpu_count_logical(): + """Return the number of logical CPUs in the system.""" + return cext.cpu_count_logical() + + +def cpu_count_physical(): + """Return the number of physical CPUs in the system.""" + return cext.cpu_count_phys() + + +def cpu_stats(): + ctx_switches, interrupts, soft_interrupts, syscalls, traps = \ + cext.cpu_stats() + return _common.scpustats( + ctx_switches, interrupts, soft_interrupts, syscalls) + + +def cpu_freq(): + """Return CPU frequency. + On macOS per-cpu frequency is not supported. + Also, the returned frequency never changes, see: + https://arstechnica.com/civis/viewtopic.php?f=19&t=465002 + """ + curr, min_, max_ = cext.cpu_freq() + return [_common.scpufreq(curr, min_, max_)] + + +# ===================================================================== +# --- disks +# ===================================================================== + + +disk_usage = _psposix.disk_usage +disk_io_counters = cext.disk_io_counters + + +def disk_partitions(all=False): + """Return mounted disk partitions as a list of namedtuples.""" + retlist = [] + partitions = cext.disk_partitions() + for partition in partitions: + device, mountpoint, fstype, opts = partition + if device == 'none': + device = '' + if not all: + if not os.path.isabs(device) or not os.path.exists(device): + continue + ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + retlist.append(ntuple) + return retlist + + +# ===================================================================== +# --- sensors +# ===================================================================== + + +def sensors_battery(): + """Return battery information.""" + try: + percent, minsleft, power_plugged = cext.sensors_battery() + except NotImplementedError: + # no power source - return None according to interface + return None + power_plugged = power_plugged == 1 + if power_plugged: + secsleft = _common.POWER_TIME_UNLIMITED + elif minsleft == -1: + secsleft = _common.POWER_TIME_UNKNOWN + else: + secsleft = minsleft * 60 + return _common.sbattery(percent, secsleft, power_plugged) + + +# ===================================================================== +# --- network +# ===================================================================== + + +net_io_counters = cext.net_io_counters +net_if_addrs = cext_posix.net_if_addrs + + +def net_connections(kind='inet'): + """System-wide network connections.""" + # Note: on macOS this will fail with AccessDenied unless + # the process is owned by root. + ret = [] + for pid in pids(): + try: + cons = Process(pid).connections(kind) + except NoSuchProcess: + continue + else: + if cons: + for c in cons: + c = list(c) + [pid] + ret.append(_common.sconn(*c)) + return ret + + +def net_if_stats(): + """Get NIC stats (isup, duplex, speed, mtu).""" + names = net_io_counters().keys() + ret = {} + for name in names: + try: + mtu = cext_posix.net_if_mtu(name) + isup = cext_posix.net_if_flags(name) + duplex, speed = cext_posix.net_if_duplex_speed(name) + except OSError as err: + # https://github.com/giampaolo/psutil/issues/1279 + if err.errno != errno.ENODEV: + raise + else: + if hasattr(_common, 'NicDuplex'): + duplex = _common.NicDuplex(duplex) + ret[name] = _common.snicstats(isup, duplex, speed, mtu) + return ret + + +# ===================================================================== +# --- other system functions +# ===================================================================== + + +def boot_time(): + """The system boot time expressed in seconds since the epoch.""" + return cext.boot_time() + + +def users(): + """Return currently connected users as a list of namedtuples.""" + retlist = [] + rawlist = cext.users() + for item in rawlist: + user, tty, hostname, tstamp, pid = item + if tty == '~': + continue # reboot or shutdown + if not tstamp: + continue + nt = _common.suser(user, tty or None, hostname or None, tstamp, pid) + retlist.append(nt) + return retlist + + +# ===================================================================== +# --- processes +# ===================================================================== + + +def pids(): + ls = cext.pids() + if 0 not in ls: + # On certain macOS versions pids() C doesn't return PID 0 but + # "ps" does and the process is querable via sysctl(): + # https://travis-ci.org/giampaolo/psutil/jobs/309619941 + try: + Process(0).create_time() + ls.insert(0, 0) + except NoSuchProcess: + pass + except AccessDenied: + ls.insert(0, 0) + return ls + + +pid_exists = _psposix.pid_exists + + +def wrap_exceptions(fun): + """Decorator which translates bare OSError exceptions into + NoSuchProcess and AccessDenied. + """ + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + try: + return fun(self, *args, **kwargs) + except ProcessLookupError: + raise NoSuchProcess(self.pid, self._name) + except PermissionError: + raise AccessDenied(self.pid, self._name) + except cext.ZombieProcessError: + raise ZombieProcess(self.pid, self._name, self._ppid) + return wrapper + + +@contextlib.contextmanager +def catch_zombie(proc): + """There are some poor C APIs which incorrectly raise ESRCH when + the process is still alive or it's a zombie, or even RuntimeError + (those who don't set errno). This is here in order to solve: + https://github.com/giampaolo/psutil/issues/1044 + """ + try: + yield + except (OSError, RuntimeError) as err: + if isinstance(err, RuntimeError) or err.errno == errno.ESRCH: + try: + # status() is not supposed to lie and correctly detect + # zombies so if it raises ESRCH it's true. + status = proc.status() + except NoSuchProcess: + raise err + else: + if status == _common.STATUS_ZOMBIE: + raise ZombieProcess(proc.pid, proc._name, proc._ppid) + else: + raise AccessDenied(proc.pid, proc._name) + else: + raise + + +class Process(object): + """Wrapper class around underlying C implementation.""" + + __slots__ = ["pid", "_name", "_ppid", "_cache"] + + def __init__(self, pid): + self.pid = pid + self._name = None + self._ppid = None + + @wrap_exceptions + @memoize_when_activated + def _get_kinfo_proc(self): + # Note: should work with all PIDs without permission issues. + ret = cext.proc_kinfo_oneshot(self.pid) + assert len(ret) == len(kinfo_proc_map) + return ret + + @wrap_exceptions + @memoize_when_activated + def _get_pidtaskinfo(self): + # Note: should work for PIDs owned by user only. + with catch_zombie(self): + ret = cext.proc_pidtaskinfo_oneshot(self.pid) + assert len(ret) == len(pidtaskinfo_map) + return ret + + def oneshot_enter(self): + self._get_kinfo_proc.cache_activate(self) + self._get_pidtaskinfo.cache_activate(self) + + def oneshot_exit(self): + self._get_kinfo_proc.cache_deactivate(self) + self._get_pidtaskinfo.cache_deactivate(self) + + @wrap_exceptions + def name(self): + name = self._get_kinfo_proc()[kinfo_proc_map['name']] + return name if name is not None else cext.proc_name(self.pid) + + @wrap_exceptions + def exe(self): + with catch_zombie(self): + return cext.proc_exe(self.pid) + + @wrap_exceptions + def cmdline(self): + with catch_zombie(self): + return cext.proc_cmdline(self.pid) + + @wrap_exceptions + def environ(self): + with catch_zombie(self): + return parse_environ_block(cext.proc_environ(self.pid)) + + @wrap_exceptions + def ppid(self): + self._ppid = self._get_kinfo_proc()[kinfo_proc_map['ppid']] + return self._ppid + + @wrap_exceptions + def cwd(self): + with catch_zombie(self): + return cext.proc_cwd(self.pid) + + @wrap_exceptions + def uids(self): + rawtuple = self._get_kinfo_proc() + return _common.puids( + rawtuple[kinfo_proc_map['ruid']], + rawtuple[kinfo_proc_map['euid']], + rawtuple[kinfo_proc_map['suid']]) + + @wrap_exceptions + def gids(self): + rawtuple = self._get_kinfo_proc() + return _common.puids( + rawtuple[kinfo_proc_map['rgid']], + rawtuple[kinfo_proc_map['egid']], + rawtuple[kinfo_proc_map['sgid']]) + + @wrap_exceptions + def terminal(self): + tty_nr = self._get_kinfo_proc()[kinfo_proc_map['ttynr']] + tmap = _psposix.get_terminal_map() + try: + return tmap[tty_nr] + except KeyError: + return None + + @wrap_exceptions + def memory_info(self): + rawtuple = self._get_pidtaskinfo() + return pmem( + rawtuple[pidtaskinfo_map['rss']], + rawtuple[pidtaskinfo_map['vms']], + rawtuple[pidtaskinfo_map['pfaults']], + rawtuple[pidtaskinfo_map['pageins']], + ) + + @wrap_exceptions + def memory_full_info(self): + basic_mem = self.memory_info() + uss = cext.proc_memory_uss(self.pid) + return pfullmem(*basic_mem + (uss, )) + + @wrap_exceptions + def cpu_times(self): + rawtuple = self._get_pidtaskinfo() + return _common.pcputimes( + rawtuple[pidtaskinfo_map['cpuutime']], + rawtuple[pidtaskinfo_map['cpustime']], + # children user / system times are not retrievable (set to 0) + 0.0, 0.0) + + @wrap_exceptions + def create_time(self): + return self._get_kinfo_proc()[kinfo_proc_map['ctime']] + + @wrap_exceptions + def num_ctx_switches(self): + # Unvoluntary value seems not to be available; + # getrusage() numbers seems to confirm this theory. + # We set it to 0. + vol = self._get_pidtaskinfo()[pidtaskinfo_map['volctxsw']] + return _common.pctxsw(vol, 0) + + @wrap_exceptions + def num_threads(self): + return self._get_pidtaskinfo()[pidtaskinfo_map['numthreads']] + + @wrap_exceptions + def open_files(self): + if self.pid == 0: + return [] + files = [] + with catch_zombie(self): + rawlist = cext.proc_open_files(self.pid) + for path, fd in rawlist: + if isfile_strict(path): + ntuple = _common.popenfile(path, fd) + files.append(ntuple) + return files + + @wrap_exceptions + def connections(self, kind='inet'): + if kind not in conn_tmap: + raise ValueError("invalid %r kind argument; choose between %s" + % (kind, ', '.join([repr(x) for x in conn_tmap]))) + families, types = conn_tmap[kind] + with catch_zombie(self): + rawlist = cext.proc_connections(self.pid, families, types) + ret = [] + for item in rawlist: + fd, fam, type, laddr, raddr, status = item + nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, + TCP_STATUSES) + ret.append(nt) + return ret + + @wrap_exceptions + def num_fds(self): + if self.pid == 0: + return 0 + with catch_zombie(self): + return cext.proc_num_fds(self.pid) + + @wrap_exceptions + def wait(self, timeout=None): + return _psposix.wait_pid(self.pid, timeout, self._name) + + @wrap_exceptions + def nice_get(self): + with catch_zombie(self): + return cext_posix.getpriority(self.pid) + + @wrap_exceptions + def nice_set(self, value): + with catch_zombie(self): + return cext_posix.setpriority(self.pid, value) + + @wrap_exceptions + def status(self): + code = self._get_kinfo_proc()[kinfo_proc_map['status']] + # XXX is '?' legit? (we're not supposed to return it anyway) + return PROC_STATUSES.get(code, '?') + + @wrap_exceptions + def threads(self): + rawlist = cext.proc_threads(self.pid) + retlist = [] + for thread_id, utime, stime in rawlist: + ntuple = _common.pthread(thread_id, utime, stime) + retlist.append(ntuple) + return retlist diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psposix.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psposix.py new file mode 100644 index 000000000..24570224f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psposix.py @@ -0,0 +1,179 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Routines common to all posix systems.""" + +import glob +import os +import sys +import time + +from ._common import memoize +from ._common import sdiskusage +from ._common import usage_percent +from ._compat import ChildProcessError +from ._compat import FileNotFoundError +from ._compat import InterruptedError +from ._compat import PermissionError +from ._compat import ProcessLookupError +from ._compat import PY3 +from ._compat import unicode + + +__all__ = ['pid_exists', 'wait_pid', 'disk_usage', 'get_terminal_map'] + + +# This object gets set on "import psutil" from the __init__.py +# file, see: https://github.com/giampaolo/psutil/issues/1402 +TimeoutExpired = None + + +def pid_exists(pid): + """Check whether pid exists in the current process table.""" + if pid == 0: + # According to "man 2 kill" PID 0 has a special meaning: + # it refers to <> so we don't want to go any further. + # If we get here it means this UNIX platform *does* have + # a process with id 0. + return True + try: + os.kill(pid, 0) + except ProcessLookupError: + return False + except PermissionError: + # EPERM clearly means there's a process to deny access to + return True + # According to "man 2 kill" possible error values are + # (EINVAL, EPERM, ESRCH) + else: + return True + + +def wait_pid(pid, timeout=None, proc_name=None): + """Wait for process with pid 'pid' to terminate and return its + exit status code as an integer. + + If pid is not a children of os.getpid() (current process) just + waits until the process disappears and return None. + + If pid does not exist at all return None immediately. + + Raise TimeoutExpired on timeout expired. + """ + def check_timeout(delay): + if timeout is not None: + if timer() >= stop_at: + raise TimeoutExpired(timeout, pid=pid, name=proc_name) + time.sleep(delay) + return min(delay * 2, 0.04) + + timer = getattr(time, 'monotonic', time.time) + if timeout is not None: + def waitcall(): + return os.waitpid(pid, os.WNOHANG) + stop_at = timer() + timeout + else: + def waitcall(): + return os.waitpid(pid, 0) + + delay = 0.0001 + while True: + try: + retpid, status = waitcall() + except InterruptedError: + delay = check_timeout(delay) + except ChildProcessError: + # This has two meanings: + # - pid is not a child of os.getpid() in which case + # we keep polling until it's gone + # - pid never existed in the first place + # In both cases we'll eventually return None as we + # can't determine its exit status code. + while True: + if pid_exists(pid): + delay = check_timeout(delay) + else: + return + else: + if retpid == 0: + # WNOHANG was used, pid is still running + delay = check_timeout(delay) + continue + # process exited due to a signal; return the integer of + # that signal + if os.WIFSIGNALED(status): + return -os.WTERMSIG(status) + # process exited using exit(2) system call; return the + # integer exit(2) system call has been called with + elif os.WIFEXITED(status): + return os.WEXITSTATUS(status) + else: + # should never happen + raise ValueError("unknown process exit status %r" % status) + + +def disk_usage(path): + """Return disk usage associated with path. + Note: UNIX usually reserves 5% disk space which is not accessible + by user. In this function "total" and "used" values reflect the + total and used disk space whereas "free" and "percent" represent + the "free" and "used percent" user disk space. + """ + if PY3: + st = os.statvfs(path) + else: + # os.statvfs() does not support unicode on Python 2: + # - https://github.com/giampaolo/psutil/issues/416 + # - http://bugs.python.org/issue18695 + try: + st = os.statvfs(path) + except UnicodeEncodeError: + if isinstance(path, unicode): + try: + path = path.encode(sys.getfilesystemencoding()) + except UnicodeEncodeError: + pass + st = os.statvfs(path) + else: + raise + + # Total space which is only available to root (unless changed + # at system level). + total = (st.f_blocks * st.f_frsize) + # Remaining free space usable by root. + avail_to_root = (st.f_bfree * st.f_frsize) + # Remaining free space usable by user. + avail_to_user = (st.f_bavail * st.f_frsize) + # Total space being used in general. + used = (total - avail_to_root) + # Total space which is available to user (same as 'total' but + # for the user). + total_user = used + avail_to_user + # User usage percent compared to the total amount of space + # the user can use. This number would be higher if compared + # to root's because the user has less space (usually -5%). + usage_percent_user = usage_percent(used, total_user, round_=1) + + # NB: the percentage is -5% than what shown by df due to + # reserved blocks that we are currently not considering: + # https://github.com/giampaolo/psutil/issues/829#issuecomment-223750462 + return sdiskusage( + total=total, used=used, free=avail_to_user, percent=usage_percent_user) + + +@memoize +def get_terminal_map(): + """Get a map of device-id -> path as a dict. + Used by Process.terminal() + """ + ret = {} + ls = glob.glob('/dev/tty*') + glob.glob('/dev/pts/*') + for name in ls: + assert name not in ret, name + try: + ret[os.stat(name).st_rdev] = name + except FileNotFoundError: + pass + return ret diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_pssunos.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_pssunos.py new file mode 100644 index 000000000..2aa2a8661 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_pssunos.py @@ -0,0 +1,720 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Sun OS Solaris platform implementation.""" + +import errno +import functools +import os +import socket +import subprocess +import sys +from collections import namedtuple +from socket import AF_INET + +from . import _common +from . import _psposix +from . import _psutil_posix as cext_posix +from . import _psutil_sunos as cext +from ._common import AF_INET6 +from ._common import get_procfs_path +from ._common import isfile_strict +from ._common import memoize_when_activated +from ._common import sockfam_to_enum +from ._common import socktype_to_enum +from ._common import usage_percent +from ._compat import b +from ._compat import FileNotFoundError +from ._compat import PermissionError +from ._compat import ProcessLookupError +from ._compat import PY3 + + +__extra__all__ = ["CONN_IDLE", "CONN_BOUND", "PROCFS_PATH"] + + +# ===================================================================== +# --- globals +# ===================================================================== + + +PAGE_SIZE = os.sysconf('SC_PAGE_SIZE') +AF_LINK = cext_posix.AF_LINK +IS_64_BIT = sys.maxsize > 2**32 + +CONN_IDLE = "IDLE" +CONN_BOUND = "BOUND" + +PROC_STATUSES = { + cext.SSLEEP: _common.STATUS_SLEEPING, + cext.SRUN: _common.STATUS_RUNNING, + cext.SZOMB: _common.STATUS_ZOMBIE, + cext.SSTOP: _common.STATUS_STOPPED, + cext.SIDL: _common.STATUS_IDLE, + cext.SONPROC: _common.STATUS_RUNNING, # same as run + cext.SWAIT: _common.STATUS_WAITING, +} + +TCP_STATUSES = { + cext.TCPS_ESTABLISHED: _common.CONN_ESTABLISHED, + cext.TCPS_SYN_SENT: _common.CONN_SYN_SENT, + cext.TCPS_SYN_RCVD: _common.CONN_SYN_RECV, + cext.TCPS_FIN_WAIT_1: _common.CONN_FIN_WAIT1, + cext.TCPS_FIN_WAIT_2: _common.CONN_FIN_WAIT2, + cext.TCPS_TIME_WAIT: _common.CONN_TIME_WAIT, + cext.TCPS_CLOSED: _common.CONN_CLOSE, + cext.TCPS_CLOSE_WAIT: _common.CONN_CLOSE_WAIT, + cext.TCPS_LAST_ACK: _common.CONN_LAST_ACK, + cext.TCPS_LISTEN: _common.CONN_LISTEN, + cext.TCPS_CLOSING: _common.CONN_CLOSING, + cext.PSUTIL_CONN_NONE: _common.CONN_NONE, + cext.TCPS_IDLE: CONN_IDLE, # sunos specific + cext.TCPS_BOUND: CONN_BOUND, # sunos specific +} + +proc_info_map = dict( + ppid=0, + rss=1, + vms=2, + create_time=3, + nice=4, + num_threads=5, + status=6, + ttynr=7, + uid=8, + euid=9, + gid=10, + egid=11) + +# These objects get set on "import psutil" from the __init__.py +# file, see: https://github.com/giampaolo/psutil/issues/1402 +NoSuchProcess = None +ZombieProcess = None +AccessDenied = None +TimeoutExpired = None + + +# ===================================================================== +# --- named tuples +# ===================================================================== + + +# psutil.cpu_times() +scputimes = namedtuple('scputimes', ['user', 'system', 'idle', 'iowait']) +# psutil.cpu_times(percpu=True) +pcputimes = namedtuple('pcputimes', + ['user', 'system', 'children_user', 'children_system']) +# psutil.virtual_memory() +svmem = namedtuple('svmem', ['total', 'available', 'percent', 'used', 'free']) +# psutil.Process.memory_info() +pmem = namedtuple('pmem', ['rss', 'vms']) +pfullmem = pmem +# psutil.Process.memory_maps(grouped=True) +pmmap_grouped = namedtuple('pmmap_grouped', + ['path', 'rss', 'anonymous', 'locked']) +# psutil.Process.memory_maps(grouped=False) +pmmap_ext = namedtuple( + 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields)) + + +# ===================================================================== +# --- memory +# ===================================================================== + + +def virtual_memory(): + """Report virtual memory metrics.""" + # we could have done this with kstat, but IMHO this is good enough + total = os.sysconf('SC_PHYS_PAGES') * PAGE_SIZE + # note: there's no difference on Solaris + free = avail = os.sysconf('SC_AVPHYS_PAGES') * PAGE_SIZE + used = total - free + percent = usage_percent(used, total, round_=1) + return svmem(total, avail, percent, used, free) + + +def swap_memory(): + """Report swap memory metrics.""" + sin, sout = cext.swap_mem() + # XXX + # we are supposed to get total/free by doing so: + # http://cvs.opensolaris.org/source/xref/onnv/onnv-gate/ + # usr/src/cmd/swap/swap.c + # ...nevertheless I can't manage to obtain the same numbers as 'swap' + # cmdline utility, so let's parse its output (sigh!) + p = subprocess.Popen(['/usr/bin/env', 'PATH=/usr/sbin:/sbin:%s' % + os.environ['PATH'], 'swap', '-l'], + stdout=subprocess.PIPE) + stdout, stderr = p.communicate() + if PY3: + stdout = stdout.decode(sys.stdout.encoding) + if p.returncode != 0: + raise RuntimeError("'swap -l' failed (retcode=%s)" % p.returncode) + + lines = stdout.strip().split('\n')[1:] + if not lines: + raise RuntimeError('no swap device(s) configured') + total = free = 0 + for line in lines: + line = line.split() + t, f = line[-2:] + total += int(int(t) * 512) + free += int(int(f) * 512) + used = total - free + percent = usage_percent(used, total, round_=1) + return _common.sswap(total, used, free, percent, + sin * PAGE_SIZE, sout * PAGE_SIZE) + + +# ===================================================================== +# --- CPU +# ===================================================================== + + +def cpu_times(): + """Return system-wide CPU times as a named tuple""" + ret = cext.per_cpu_times() + return scputimes(*[sum(x) for x in zip(*ret)]) + + +def per_cpu_times(): + """Return system per-CPU times as a list of named tuples""" + ret = cext.per_cpu_times() + return [scputimes(*x) for x in ret] + + +def cpu_count_logical(): + """Return the number of logical CPUs in the system.""" + try: + return os.sysconf("SC_NPROCESSORS_ONLN") + except ValueError: + # mimic os.cpu_count() behavior + return None + + +def cpu_count_physical(): + """Return the number of physical CPUs in the system.""" + return cext.cpu_count_phys() + + +def cpu_stats(): + """Return various CPU stats as a named tuple.""" + ctx_switches, interrupts, syscalls, traps = cext.cpu_stats() + soft_interrupts = 0 + return _common.scpustats(ctx_switches, interrupts, soft_interrupts, + syscalls) + + +# ===================================================================== +# --- disks +# ===================================================================== + + +disk_io_counters = cext.disk_io_counters +disk_usage = _psposix.disk_usage + + +def disk_partitions(all=False): + """Return system disk partitions.""" + # TODO - the filtering logic should be better checked so that + # it tries to reflect 'df' as much as possible + retlist = [] + partitions = cext.disk_partitions() + for partition in partitions: + device, mountpoint, fstype, opts = partition + if device == 'none': + device = '' + if not all: + # Differently from, say, Linux, we don't have a list of + # common fs types so the best we can do, AFAIK, is to + # filter by filesystem having a total size > 0. + if not disk_usage(mountpoint).total: + continue + ntuple = _common.sdiskpart(device, mountpoint, fstype, opts) + retlist.append(ntuple) + return retlist + + +# ===================================================================== +# --- network +# ===================================================================== + + +net_io_counters = cext.net_io_counters +net_if_addrs = cext_posix.net_if_addrs + + +def net_connections(kind, _pid=-1): + """Return socket connections. If pid == -1 return system-wide + connections (as opposed to connections opened by one process only). + Only INET sockets are returned (UNIX are not). + """ + cmap = _common.conn_tmap.copy() + if _pid == -1: + cmap.pop('unix', 0) + if kind not in cmap: + raise ValueError("invalid %r kind argument; choose between %s" + % (kind, ', '.join([repr(x) for x in cmap]))) + families, types = _common.conn_tmap[kind] + rawlist = cext.net_connections(_pid) + ret = set() + for item in rawlist: + fd, fam, type_, laddr, raddr, status, pid = item + if fam not in families: + continue + if type_ not in types: + continue + # TODO: refactor and use _common.conn_to_ntuple. + if fam in (AF_INET, AF_INET6): + if laddr: + laddr = _common.addr(*laddr) + if raddr: + raddr = _common.addr(*raddr) + status = TCP_STATUSES[status] + fam = sockfam_to_enum(fam) + type_ = socktype_to_enum(type_) + if _pid == -1: + nt = _common.sconn(fd, fam, type_, laddr, raddr, status, pid) + else: + nt = _common.pconn(fd, fam, type_, laddr, raddr, status) + ret.add(nt) + return list(ret) + + +def net_if_stats(): + """Get NIC stats (isup, duplex, speed, mtu).""" + ret = cext.net_if_stats() + for name, items in ret.items(): + isup, duplex, speed, mtu = items + if hasattr(_common, 'NicDuplex'): + duplex = _common.NicDuplex(duplex) + ret[name] = _common.snicstats(isup, duplex, speed, mtu) + return ret + + +# ===================================================================== +# --- other system functions +# ===================================================================== + + +def boot_time(): + """The system boot time expressed in seconds since the epoch.""" + return cext.boot_time() + + +def users(): + """Return currently connected users as a list of namedtuples.""" + retlist = [] + rawlist = cext.users() + localhost = (':0.0', ':0') + for item in rawlist: + user, tty, hostname, tstamp, user_process, pid = item + # note: the underlying C function includes entries about + # system boot, run level and others. We might want + # to use them in the future. + if not user_process: + continue + if hostname in localhost: + hostname = 'localhost' + nt = _common.suser(user, tty, hostname, tstamp, pid) + retlist.append(nt) + return retlist + + +# ===================================================================== +# --- processes +# ===================================================================== + + +def pids(): + """Returns a list of PIDs currently running on the system.""" + return [int(x) for x in os.listdir(b(get_procfs_path())) if x.isdigit()] + + +def pid_exists(pid): + """Check for the existence of a unix pid.""" + return _psposix.pid_exists(pid) + + +def wrap_exceptions(fun): + """Call callable into a try/except clause and translate ENOENT, + EACCES and EPERM in NoSuchProcess or AccessDenied exceptions. + """ + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + try: + return fun(self, *args, **kwargs) + except (FileNotFoundError, ProcessLookupError): + # ENOENT (no such file or directory) gets raised on open(). + # ESRCH (no such process) can get raised on read() if + # process is gone in meantime. + if not pid_exists(self.pid): + raise NoSuchProcess(self.pid, self._name) + else: + raise ZombieProcess(self.pid, self._name, self._ppid) + except PermissionError: + raise AccessDenied(self.pid, self._name) + except OSError: + if self.pid == 0: + if 0 in pids(): + raise AccessDenied(self.pid, self._name) + else: + raise + raise + return wrapper + + +class Process(object): + """Wrapper class around underlying C implementation.""" + + __slots__ = ["pid", "_name", "_ppid", "_procfs_path", "_cache"] + + def __init__(self, pid): + self.pid = pid + self._name = None + self._ppid = None + self._procfs_path = get_procfs_path() + + def _assert_alive(self): + """Raise NSP if the process disappeared on us.""" + # For those C function who do not raise NSP, possibly returning + # incorrect or incomplete result. + os.stat('%s/%s' % (self._procfs_path, self.pid)) + + def oneshot_enter(self): + self._proc_name_and_args.cache_activate(self) + self._proc_basic_info.cache_activate(self) + self._proc_cred.cache_activate(self) + + def oneshot_exit(self): + self._proc_name_and_args.cache_deactivate(self) + self._proc_basic_info.cache_deactivate(self) + self._proc_cred.cache_deactivate(self) + + @wrap_exceptions + @memoize_when_activated + def _proc_name_and_args(self): + return cext.proc_name_and_args(self.pid, self._procfs_path) + + @wrap_exceptions + @memoize_when_activated + def _proc_basic_info(self): + ret = cext.proc_basic_info(self.pid, self._procfs_path) + assert len(ret) == len(proc_info_map) + return ret + + @wrap_exceptions + @memoize_when_activated + def _proc_cred(self): + return cext.proc_cred(self.pid, self._procfs_path) + + @wrap_exceptions + def name(self): + # note: max len == 15 + return self._proc_name_and_args()[0] + + @wrap_exceptions + def exe(self): + try: + return os.readlink( + "%s/%s/path/a.out" % (self._procfs_path, self.pid)) + except OSError: + pass # continue and guess the exe name from the cmdline + # Will be guessed later from cmdline but we want to explicitly + # invoke cmdline here in order to get an AccessDenied + # exception if the user has not enough privileges. + self.cmdline() + return "" + + @wrap_exceptions + def cmdline(self): + return self._proc_name_and_args()[1].split(' ') + + @wrap_exceptions + def environ(self): + return cext.proc_environ(self.pid, self._procfs_path) + + @wrap_exceptions + def create_time(self): + return self._proc_basic_info()[proc_info_map['create_time']] + + @wrap_exceptions + def num_threads(self): + return self._proc_basic_info()[proc_info_map['num_threads']] + + @wrap_exceptions + def nice_get(self): + # Note #1: getpriority(3) doesn't work for realtime processes. + # Psinfo is what ps uses, see: + # https://github.com/giampaolo/psutil/issues/1194 + return self._proc_basic_info()[proc_info_map['nice']] + + @wrap_exceptions + def nice_set(self, value): + if self.pid in (2, 3): + # Special case PIDs: internally setpriority(3) return ESRCH + # (no such process), no matter what. + # The process actually exists though, as it has a name, + # creation time, etc. + raise AccessDenied(self.pid, self._name) + return cext_posix.setpriority(self.pid, value) + + @wrap_exceptions + def ppid(self): + self._ppid = self._proc_basic_info()[proc_info_map['ppid']] + return self._ppid + + @wrap_exceptions + def uids(self): + try: + real, effective, saved, _, _, _ = self._proc_cred() + except AccessDenied: + real = self._proc_basic_info()[proc_info_map['uid']] + effective = self._proc_basic_info()[proc_info_map['euid']] + saved = None + return _common.puids(real, effective, saved) + + @wrap_exceptions + def gids(self): + try: + _, _, _, real, effective, saved = self._proc_cred() + except AccessDenied: + real = self._proc_basic_info()[proc_info_map['gid']] + effective = self._proc_basic_info()[proc_info_map['egid']] + saved = None + return _common.puids(real, effective, saved) + + @wrap_exceptions + def cpu_times(self): + try: + times = cext.proc_cpu_times(self.pid, self._procfs_path) + except OSError as err: + if err.errno == errno.EOVERFLOW and not IS_64_BIT: + # We may get here if we attempt to query a 64bit process + # with a 32bit python. + # Error originates from read() and also tools like "cat" + # fail in the same way (!). + # Since there simply is no way to determine CPU times we + # return 0.0 as a fallback. See: + # https://github.com/giampaolo/psutil/issues/857 + times = (0.0, 0.0, 0.0, 0.0) + else: + raise + return _common.pcputimes(*times) + + @wrap_exceptions + def cpu_num(self): + return cext.proc_cpu_num(self.pid, self._procfs_path) + + @wrap_exceptions + def terminal(self): + procfs_path = self._procfs_path + hit_enoent = False + tty = wrap_exceptions( + self._proc_basic_info()[proc_info_map['ttynr']]) + if tty != cext.PRNODEV: + for x in (0, 1, 2, 255): + try: + return os.readlink( + '%s/%d/path/%d' % (procfs_path, self.pid, x)) + except FileNotFoundError: + hit_enoent = True + continue + if hit_enoent: + self._assert_alive() + + @wrap_exceptions + def cwd(self): + # /proc/PID/path/cwd may not be resolved by readlink() even if + # it exists (ls shows it). If that's the case and the process + # is still alive return None (we can return None also on BSD). + # Reference: http://goo.gl/55XgO + procfs_path = self._procfs_path + try: + return os.readlink("%s/%s/path/cwd" % (procfs_path, self.pid)) + except FileNotFoundError: + os.stat("%s/%s" % (procfs_path, self.pid)) # raise NSP or AD + return None + + @wrap_exceptions + def memory_info(self): + ret = self._proc_basic_info() + rss = ret[proc_info_map['rss']] * 1024 + vms = ret[proc_info_map['vms']] * 1024 + return pmem(rss, vms) + + memory_full_info = memory_info + + @wrap_exceptions + def status(self): + code = self._proc_basic_info()[proc_info_map['status']] + # XXX is '?' legit? (we're not supposed to return it anyway) + return PROC_STATUSES.get(code, '?') + + @wrap_exceptions + def threads(self): + procfs_path = self._procfs_path + ret = [] + tids = os.listdir('%s/%d/lwp' % (procfs_path, self.pid)) + hit_enoent = False + for tid in tids: + tid = int(tid) + try: + utime, stime = cext.query_process_thread( + self.pid, tid, procfs_path) + except EnvironmentError as err: + if err.errno == errno.EOVERFLOW and not IS_64_BIT: + # We may get here if we attempt to query a 64bit process + # with a 32bit python. + # Error originates from read() and also tools like "cat" + # fail in the same way (!). + # Since there simply is no way to determine CPU times we + # return 0.0 as a fallback. See: + # https://github.com/giampaolo/psutil/issues/857 + continue + # ENOENT == thread gone in meantime + if err.errno == errno.ENOENT: + hit_enoent = True + continue + raise + else: + nt = _common.pthread(tid, utime, stime) + ret.append(nt) + if hit_enoent: + self._assert_alive() + return ret + + @wrap_exceptions + def open_files(self): + retlist = [] + hit_enoent = False + procfs_path = self._procfs_path + pathdir = '%s/%d/path' % (procfs_path, self.pid) + for fd in os.listdir('%s/%d/fd' % (procfs_path, self.pid)): + path = os.path.join(pathdir, fd) + if os.path.islink(path): + try: + file = os.readlink(path) + except FileNotFoundError: + hit_enoent = True + continue + else: + if isfile_strict(file): + retlist.append(_common.popenfile(file, int(fd))) + if hit_enoent: + self._assert_alive() + return retlist + + def _get_unix_sockets(self, pid): + """Get UNIX sockets used by process by parsing 'pfiles' output.""" + # TODO: rewrite this in C (...but the damn netstat source code + # does not include this part! Argh!!) + cmd = "pfiles %s" % pid + p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + if PY3: + stdout, stderr = [x.decode(sys.stdout.encoding) + for x in (stdout, stderr)] + if p.returncode != 0: + if 'permission denied' in stderr.lower(): + raise AccessDenied(self.pid, self._name) + if 'no such process' in stderr.lower(): + raise NoSuchProcess(self.pid, self._name) + raise RuntimeError("%r command error\n%s" % (cmd, stderr)) + + lines = stdout.split('\n')[2:] + for i, line in enumerate(lines): + line = line.lstrip() + if line.startswith('sockname: AF_UNIX'): + path = line.split(' ', 2)[2] + type = lines[i - 2].strip() + if type == 'SOCK_STREAM': + type = socket.SOCK_STREAM + elif type == 'SOCK_DGRAM': + type = socket.SOCK_DGRAM + else: + type = -1 + yield (-1, socket.AF_UNIX, type, path, "", _common.CONN_NONE) + + @wrap_exceptions + def connections(self, kind='inet'): + ret = net_connections(kind, _pid=self.pid) + # The underlying C implementation retrieves all OS connections + # and filters them by PID. At this point we can't tell whether + # an empty list means there were no connections for process or + # process is no longer active so we force NSP in case the PID + # is no longer there. + if not ret: + # will raise NSP if process is gone + os.stat('%s/%s' % (self._procfs_path, self.pid)) + + # UNIX sockets + if kind in ('all', 'unix'): + ret.extend([_common.pconn(*conn) for conn in + self._get_unix_sockets(self.pid)]) + return ret + + nt_mmap_grouped = namedtuple('mmap', 'path rss anon locked') + nt_mmap_ext = namedtuple('mmap', 'addr perms path rss anon locked') + + @wrap_exceptions + def memory_maps(self): + def toaddr(start, end): + return '%s-%s' % (hex(start)[2:].strip('L'), + hex(end)[2:].strip('L')) + + procfs_path = self._procfs_path + retlist = [] + try: + rawlist = cext.proc_memory_maps(self.pid, procfs_path) + except OSError as err: + if err.errno == errno.EOVERFLOW and not IS_64_BIT: + # We may get here if we attempt to query a 64bit process + # with a 32bit python. + # Error originates from read() and also tools like "cat" + # fail in the same way (!). + # Since there simply is no way to determine CPU times we + # return 0.0 as a fallback. See: + # https://github.com/giampaolo/psutil/issues/857 + return [] + else: + raise + hit_enoent = False + for item in rawlist: + addr, addrsize, perm, name, rss, anon, locked = item + addr = toaddr(addr, addrsize) + if not name.startswith('['): + try: + name = os.readlink( + '%s/%s/path/%s' % (procfs_path, self.pid, name)) + except OSError as err: + if err.errno == errno.ENOENT: + # sometimes the link may not be resolved by + # readlink() even if it exists (ls shows it). + # If that's the case we just return the + # unresolved link path. + # This seems an incosistency with /proc similar + # to: http://goo.gl/55XgO + name = '%s/%s/path/%s' % (procfs_path, self.pid, name) + hit_enoent = True + else: + raise + retlist.append((addr, perm, name, rss, anon, locked)) + if hit_enoent: + self._assert_alive() + return retlist + + @wrap_exceptions + def num_fds(self): + return len(os.listdir("%s/%s/fd" % (self._procfs_path, self.pid))) + + @wrap_exceptions + def num_ctx_switches(self): + return _common.pctxsw( + *cext.proc_num_ctx_switches(self.pid, self._procfs_path)) + + @wrap_exceptions + def wait(self, timeout=None): + return _psposix.wait_pid(self.pid, timeout, self._name) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psutil_osx.cpython-38-darwin.so b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psutil_osx.cpython-38-darwin.so new file mode 100755 index 000000000..58ea18788 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psutil_osx.cpython-38-darwin.so differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psutil_posix.cpython-38-darwin.so b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psutil_posix.cpython-38-darwin.so new file mode 100755 index 000000000..9075f08ee Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_psutil_posix.cpython-38-darwin.so differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_pswindows.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_pswindows.py new file mode 100644 index 000000000..636b0af90 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/_pswindows.py @@ -0,0 +1,1127 @@ +# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Windows platform implementation.""" + +import contextlib +import errno +import functools +import os +import sys +import time +from collections import namedtuple + +from . import _common +try: + from . import _psutil_windows as cext +except ImportError as err: + if str(err).lower().startswith("dll load failed") and \ + sys.getwindowsversion()[0] < 6: + # We may get here if: + # 1) we are on an old Windows version + # 2) psutil was installed via pip + wheel + # See: https://github.com/giampaolo/psutil/issues/811 + # It must be noted that psutil can still (kind of) work + # on outdated systems if compiled / installed from sources, + # but if we get here it means this this was a wheel (or exe). + msg = "this Windows version is too old (< Windows Vista); " + msg += "psutil 3.4.2 is the latest version which supports Windows " + msg += "2000, XP and 2003 server" + raise RuntimeError(msg) + else: + raise + +from ._common import conn_tmap +from ._common import conn_to_ntuple +from ._common import ENCODING +from ._common import ENCODING_ERRS +from ._common import isfile_strict +from ._common import memoize +from ._common import memoize_when_activated +from ._common import parse_environ_block +from ._common import usage_percent +from ._compat import long +from ._compat import lru_cache +from ._compat import PY3 +from ._compat import unicode +from ._compat import xrange +from ._psutil_windows import ABOVE_NORMAL_PRIORITY_CLASS +from ._psutil_windows import BELOW_NORMAL_PRIORITY_CLASS +from ._psutil_windows import HIGH_PRIORITY_CLASS +from ._psutil_windows import IDLE_PRIORITY_CLASS +from ._psutil_windows import NORMAL_PRIORITY_CLASS +from ._psutil_windows import REALTIME_PRIORITY_CLASS + +if sys.version_info >= (3, 4): + import enum +else: + enum = None + +# process priority constants, import from __init__.py: +# http://msdn.microsoft.com/en-us/library/ms686219(v=vs.85).aspx +__extra__all__ = [ + "win_service_iter", "win_service_get", + # Process priority + "ABOVE_NORMAL_PRIORITY_CLASS", "BELOW_NORMAL_PRIORITY_CLASS", + "HIGH_PRIORITY_CLASS", "IDLE_PRIORITY_CLASS", "NORMAL_PRIORITY_CLASS", + "REALTIME_PRIORITY_CLASS", + # IO priority + "IOPRIO_VERYLOW", "IOPRIO_LOW", "IOPRIO_NORMAL", "IOPRIO_HIGH", + # others + "CONN_DELETE_TCB", "AF_LINK", +] + + +# ===================================================================== +# --- globals +# ===================================================================== + +CONN_DELETE_TCB = "DELETE_TCB" +HAS_PROC_IO_PRIORITY = hasattr(cext, "proc_io_priority_get") +HAS_GETLOADAVG = hasattr(cext, "getloadavg") +ERROR_PARTIAL_COPY = 299 + + +if enum is None: + AF_LINK = -1 +else: + AddressFamily = enum.IntEnum('AddressFamily', {'AF_LINK': -1}) + AF_LINK = AddressFamily.AF_LINK + +TCP_STATUSES = { + cext.MIB_TCP_STATE_ESTAB: _common.CONN_ESTABLISHED, + cext.MIB_TCP_STATE_SYN_SENT: _common.CONN_SYN_SENT, + cext.MIB_TCP_STATE_SYN_RCVD: _common.CONN_SYN_RECV, + cext.MIB_TCP_STATE_FIN_WAIT1: _common.CONN_FIN_WAIT1, + cext.MIB_TCP_STATE_FIN_WAIT2: _common.CONN_FIN_WAIT2, + cext.MIB_TCP_STATE_TIME_WAIT: _common.CONN_TIME_WAIT, + cext.MIB_TCP_STATE_CLOSED: _common.CONN_CLOSE, + cext.MIB_TCP_STATE_CLOSE_WAIT: _common.CONN_CLOSE_WAIT, + cext.MIB_TCP_STATE_LAST_ACK: _common.CONN_LAST_ACK, + cext.MIB_TCP_STATE_LISTEN: _common.CONN_LISTEN, + cext.MIB_TCP_STATE_CLOSING: _common.CONN_CLOSING, + cext.MIB_TCP_STATE_DELETE_TCB: CONN_DELETE_TCB, + cext.PSUTIL_CONN_NONE: _common.CONN_NONE, +} + +if enum is not None: + class Priority(enum.IntEnum): + ABOVE_NORMAL_PRIORITY_CLASS = ABOVE_NORMAL_PRIORITY_CLASS + BELOW_NORMAL_PRIORITY_CLASS = BELOW_NORMAL_PRIORITY_CLASS + HIGH_PRIORITY_CLASS = HIGH_PRIORITY_CLASS + IDLE_PRIORITY_CLASS = IDLE_PRIORITY_CLASS + NORMAL_PRIORITY_CLASS = NORMAL_PRIORITY_CLASS + REALTIME_PRIORITY_CLASS = REALTIME_PRIORITY_CLASS + + globals().update(Priority.__members__) + +if enum is None: + IOPRIO_VERYLOW = 0 + IOPRIO_LOW = 1 + IOPRIO_NORMAL = 2 + IOPRIO_HIGH = 3 +else: + class IOPriority(enum.IntEnum): + IOPRIO_VERYLOW = 0 + IOPRIO_LOW = 1 + IOPRIO_NORMAL = 2 + IOPRIO_HIGH = 3 + globals().update(IOPriority.__members__) + +pinfo_map = dict( + num_handles=0, + ctx_switches=1, + user_time=2, + kernel_time=3, + create_time=4, + num_threads=5, + io_rcount=6, + io_wcount=7, + io_rbytes=8, + io_wbytes=9, + io_count_others=10, + io_bytes_others=11, + num_page_faults=12, + peak_wset=13, + wset=14, + peak_paged_pool=15, + paged_pool=16, + peak_non_paged_pool=17, + non_paged_pool=18, + pagefile=19, + peak_pagefile=20, + mem_private=21, +) + +# These objects get set on "import psutil" from the __init__.py +# file, see: https://github.com/giampaolo/psutil/issues/1402 +NoSuchProcess = None +ZombieProcess = None +AccessDenied = None +TimeoutExpired = None + +# More values at: https://stackoverflow.com/a/20804735/376587 +WIN_10 = (10, 0) +WIN_8 = (6, 2) +WIN_7 = (6, 1) +WIN_SERVER_2008 = (6, 0) +WIN_VISTA = (6, 0) +WIN_SERVER_2003 = (5, 2) +WIN_XP = (5, 1) + + +@lru_cache() +def get_winver(): + """Usage: + >>> if get_winver() <= WIN_VISTA: + ... ... + """ + wv = sys.getwindowsversion() + return (wv.major, wv.minor) + + +IS_WIN_XP = get_winver() < WIN_VISTA + + +# ===================================================================== +# --- named tuples +# ===================================================================== + + +# psutil.cpu_times() +scputimes = namedtuple('scputimes', + ['user', 'system', 'idle', 'interrupt', 'dpc']) +# psutil.virtual_memory() +svmem = namedtuple('svmem', ['total', 'available', 'percent', 'used', 'free']) +# psutil.Process.memory_info() +pmem = namedtuple( + 'pmem', ['rss', 'vms', + 'num_page_faults', 'peak_wset', 'wset', 'peak_paged_pool', + 'paged_pool', 'peak_nonpaged_pool', 'nonpaged_pool', + 'pagefile', 'peak_pagefile', 'private']) +# psutil.Process.memory_full_info() +pfullmem = namedtuple('pfullmem', pmem._fields + ('uss', )) +# psutil.Process.memory_maps(grouped=True) +pmmap_grouped = namedtuple('pmmap_grouped', ['path', 'rss']) +# psutil.Process.memory_maps(grouped=False) +pmmap_ext = namedtuple( + 'pmmap_ext', 'addr perms ' + ' '.join(pmmap_grouped._fields)) +# psutil.Process.io_counters() +pio = namedtuple('pio', ['read_count', 'write_count', + 'read_bytes', 'write_bytes', + 'other_count', 'other_bytes']) + + +# ===================================================================== +# --- utils +# ===================================================================== + + +@lru_cache(maxsize=512) +def convert_dos_path(s): + r"""Convert paths using native DOS format like: + "\Device\HarddiskVolume1\Windows\systemew\file.txt" + into: + "C:\Windows\systemew\file.txt" + """ + rawdrive = '\\'.join(s.split('\\')[:3]) + driveletter = cext.win32_QueryDosDevice(rawdrive) + return os.path.join(driveletter, s[len(rawdrive):]) + + +def py2_strencode(s): + """Encode a unicode string to a byte string by using the default fs + encoding + "replace" error handler. + """ + if PY3: + return s + else: + if isinstance(s, str): + return s + else: + return s.encode(ENCODING, ENCODING_ERRS) + + +@memoize +def getpagesize(): + return cext.getpagesize() + + +# ===================================================================== +# --- memory +# ===================================================================== + + +def virtual_memory(): + """System virtual memory as a namedtuple.""" + mem = cext.virtual_mem() + totphys, availphys, totpagef, availpagef, totvirt, freevirt = mem + # + total = totphys + avail = availphys + free = availphys + used = total - avail + percent = usage_percent((total - avail), total, round_=1) + return svmem(total, avail, percent, used, free) + + +def swap_memory(): + """Swap system memory as a (total, used, free, sin, sout) tuple.""" + mem = cext.virtual_mem() + total = mem[2] + free = mem[3] + used = total - free + percent = usage_percent(used, total, round_=1) + return _common.sswap(total, used, free, percent, 0, 0) + + +# ===================================================================== +# --- disk +# ===================================================================== + + +disk_io_counters = cext.disk_io_counters + + +def disk_usage(path): + """Return disk usage associated with path.""" + if PY3 and isinstance(path, bytes): + # XXX: do we want to use "strict"? Probably yes, in order + # to fail immediately. After all we are accepting input here... + path = path.decode(ENCODING, errors="strict") + total, free = cext.disk_usage(path) + used = total - free + percent = usage_percent(used, total, round_=1) + return _common.sdiskusage(total, used, free, percent) + + +def disk_partitions(all): + """Return disk partitions.""" + rawlist = cext.disk_partitions(all) + return [_common.sdiskpart(*x) for x in rawlist] + + +# ===================================================================== +# --- CPU +# ===================================================================== + + +def cpu_times(): + """Return system CPU times as a named tuple.""" + user, system, idle = cext.cpu_times() + # Internally, GetSystemTimes() is used, and it doesn't return + # interrupt and dpc times. cext.per_cpu_times() does, so we + # rely on it to get those only. + percpu_summed = scputimes(*[sum(n) for n in zip(*cext.per_cpu_times())]) + return scputimes(user, system, idle, + percpu_summed.interrupt, percpu_summed.dpc) + + +def per_cpu_times(): + """Return system per-CPU times as a list of named tuples.""" + ret = [] + for user, system, idle, interrupt, dpc in cext.per_cpu_times(): + item = scputimes(user, system, idle, interrupt, dpc) + ret.append(item) + return ret + + +def cpu_count_logical(): + """Return the number of logical CPUs in the system.""" + return cext.cpu_count_logical() + + +def cpu_count_physical(): + """Return the number of physical CPU cores in the system.""" + return cext.cpu_count_phys() + + +def cpu_stats(): + """Return CPU statistics.""" + ctx_switches, interrupts, dpcs, syscalls = cext.cpu_stats() + soft_interrupts = 0 + return _common.scpustats(ctx_switches, interrupts, soft_interrupts, + syscalls) + + +def cpu_freq(): + """Return CPU frequency. + On Windows per-cpu frequency is not supported. + """ + curr, max_ = cext.cpu_freq() + min_ = 0.0 + return [_common.scpufreq(float(curr), min_, float(max_))] + + +if HAS_GETLOADAVG: + _loadavg_inititialized = False + + def getloadavg(): + """Return the number of processes in the system run queue averaged + over the last 1, 5, and 15 minutes respectively as a tuple""" + global _loadavg_inititialized + + if not _loadavg_inititialized: + cext.init_loadavg_counter() + _loadavg_inititialized = True + + # Drop to 2 decimal points which is what Linux does + raw_loads = cext.getloadavg() + return tuple([round(load, 2) for load in raw_loads]) + + +# ===================================================================== +# --- network +# ===================================================================== + + +def net_connections(kind, _pid=-1): + """Return socket connections. If pid == -1 return system-wide + connections (as opposed to connections opened by one process only). + """ + if kind not in conn_tmap: + raise ValueError("invalid %r kind argument; choose between %s" + % (kind, ', '.join([repr(x) for x in conn_tmap]))) + families, types = conn_tmap[kind] + rawlist = cext.net_connections(_pid, families, types) + ret = set() + for item in rawlist: + fd, fam, type, laddr, raddr, status, pid = item + nt = conn_to_ntuple(fd, fam, type, laddr, raddr, status, TCP_STATUSES, + pid=pid if _pid == -1 else None) + ret.add(nt) + return list(ret) + + +def net_if_stats(): + """Get NIC stats (isup, duplex, speed, mtu).""" + ret = {} + rawdict = cext.net_if_stats() + for name, items in rawdict.items(): + if not PY3: + assert isinstance(name, unicode), type(name) + name = py2_strencode(name) + isup, duplex, speed, mtu = items + if hasattr(_common, 'NicDuplex'): + duplex = _common.NicDuplex(duplex) + ret[name] = _common.snicstats(isup, duplex, speed, mtu) + return ret + + +def net_io_counters(): + """Return network I/O statistics for every network interface + installed on the system as a dict of raw tuples. + """ + ret = cext.net_io_counters() + return dict([(py2_strencode(k), v) for k, v in ret.items()]) + + +def net_if_addrs(): + """Return the addresses associated to each NIC.""" + ret = [] + for items in cext.net_if_addrs(): + items = list(items) + items[0] = py2_strencode(items[0]) + ret.append(items) + return ret + + +# ===================================================================== +# --- sensors +# ===================================================================== + + +def sensors_battery(): + """Return battery information.""" + # For constants meaning see: + # https://msdn.microsoft.com/en-us/library/windows/desktop/ + # aa373232(v=vs.85).aspx + acline_status, flags, percent, secsleft = cext.sensors_battery() + power_plugged = acline_status == 1 + no_battery = bool(flags & 128) + charging = bool(flags & 8) + + if no_battery: + return None + if power_plugged or charging: + secsleft = _common.POWER_TIME_UNLIMITED + elif secsleft == -1: + secsleft = _common.POWER_TIME_UNKNOWN + + return _common.sbattery(percent, secsleft, power_plugged) + + +# ===================================================================== +# --- other system functions +# ===================================================================== + + +_last_btime = 0 + + +def boot_time(): + """The system boot time expressed in seconds since the epoch.""" + # This dirty hack is to adjust the precision of the returned + # value which may have a 1 second fluctuation, see: + # https://github.com/giampaolo/psutil/issues/1007 + global _last_btime + ret = float(cext.boot_time()) + if abs(ret - _last_btime) <= 1: + return _last_btime + else: + _last_btime = ret + return ret + + +def users(): + """Return currently connected users as a list of namedtuples.""" + retlist = [] + rawlist = cext.users() + for item in rawlist: + user, hostname, tstamp = item + user = py2_strencode(user) + nt = _common.suser(user, None, hostname, tstamp, None) + retlist.append(nt) + return retlist + + +# ===================================================================== +# --- Windows services +# ===================================================================== + + +def win_service_iter(): + """Yields a list of WindowsService instances.""" + for name, display_name in cext.winservice_enumerate(): + yield WindowsService(py2_strencode(name), py2_strencode(display_name)) + + +def win_service_get(name): + """Open a Windows service and return it as a WindowsService instance.""" + service = WindowsService(name, None) + service._display_name = service._query_config()['display_name'] + return service + + +class WindowsService(object): + """Represents an installed Windows service.""" + + def __init__(self, name, display_name): + self._name = name + self._display_name = display_name + + def __str__(self): + details = "(name=%r, display_name=%r)" % ( + self._name, self._display_name) + return "%s%s" % (self.__class__.__name__, details) + + def __repr__(self): + return "<%s at %s>" % (self.__str__(), id(self)) + + def __eq__(self, other): + # Test for equality with another WindosService object based + # on name. + if not isinstance(other, WindowsService): + return NotImplemented + return self._name == other._name + + def __ne__(self, other): + return not self == other + + def _query_config(self): + with self._wrap_exceptions(): + display_name, binpath, username, start_type = \ + cext.winservice_query_config(self._name) + # XXX - update _self.display_name? + return dict( + display_name=py2_strencode(display_name), + binpath=py2_strencode(binpath), + username=py2_strencode(username), + start_type=py2_strencode(start_type)) + + def _query_status(self): + with self._wrap_exceptions(): + status, pid = cext.winservice_query_status(self._name) + if pid == 0: + pid = None + return dict(status=status, pid=pid) + + @contextlib.contextmanager + def _wrap_exceptions(self): + """Ctx manager which translates bare OSError and WindowsError + exceptions into NoSuchProcess and AccessDenied. + """ + try: + yield + except OSError as err: + if is_permission_err(err): + raise AccessDenied( + pid=None, name=self._name, + msg="service %r is not querable (not enough privileges)" % + self._name) + elif err.winerror in (cext.ERROR_INVALID_NAME, + cext.ERROR_SERVICE_DOES_NOT_EXIST): + raise NoSuchProcess( + pid=None, name=self._name, + msg="service %r does not exist)" % self._name) + else: + raise + + # config query + + def name(self): + """The service name. This string is how a service is referenced + and can be passed to win_service_get() to get a new + WindowsService instance. + """ + return self._name + + def display_name(self): + """The service display name. The value is cached when this class + is instantiated. + """ + return self._display_name + + def binpath(self): + """The fully qualified path to the service binary/exe file as + a string, including command line arguments. + """ + return self._query_config()['binpath'] + + def username(self): + """The name of the user that owns this service.""" + return self._query_config()['username'] + + def start_type(self): + """A string which can either be "automatic", "manual" or + "disabled". + """ + return self._query_config()['start_type'] + + # status query + + def pid(self): + """The process PID, if any, else None. This can be passed + to Process class to control the service's process. + """ + return self._query_status()['pid'] + + def status(self): + """Service status as a string.""" + return self._query_status()['status'] + + def description(self): + """Service long description.""" + return py2_strencode(cext.winservice_query_descr(self.name())) + + # utils + + def as_dict(self): + """Utility method retrieving all the information above as a + dictionary. + """ + d = self._query_config() + d.update(self._query_status()) + d['name'] = self.name() + d['display_name'] = self.display_name() + d['description'] = self.description() + return d + + # actions + # XXX: the necessary C bindings for start() and stop() are + # implemented but for now I prefer not to expose them. + # I may change my mind in the future. Reasons: + # - they require Administrator privileges + # - can't implement a timeout for stop() (unless by using a thread, + # which sucks) + # - would require adding ServiceAlreadyStarted and + # ServiceAlreadyStopped exceptions, adding two new APIs. + # - we might also want to have modify(), which would basically mean + # rewriting win32serviceutil.ChangeServiceConfig, which involves a + # lot of stuff (and API constants which would pollute the API), see: + # http://pyxr.sourceforge.net/PyXR/c/python24/lib/site-packages/ + # win32/lib/win32serviceutil.py.html#0175 + # - psutil is typically about "read only" monitoring stuff; + # win_service_* APIs should only be used to retrieve a service and + # check whether it's running + + # def start(self, timeout=None): + # with self._wrap_exceptions(): + # cext.winservice_start(self.name()) + # if timeout: + # giveup_at = time.time() + timeout + # while True: + # if self.status() == "running": + # return + # else: + # if time.time() > giveup_at: + # raise TimeoutExpired(timeout) + # else: + # time.sleep(.1) + + # def stop(self): + # # Note: timeout is not implemented because it's just not + # # possible, see: + # # http://stackoverflow.com/questions/11973228/ + # with self._wrap_exceptions(): + # return cext.winservice_stop(self.name()) + + +# ===================================================================== +# --- processes +# ===================================================================== + + +pids = cext.pids +pid_exists = cext.pid_exists +ppid_map = cext.ppid_map # used internally by Process.children() + + +def is_permission_err(exc): + """Return True if this is a permission error.""" + assert isinstance(exc, OSError), exc + # On Python 2 OSError doesn't always have 'winerror'. Sometimes + # it does, in which case the original exception was WindowsError + # (which is a subclass of OSError). + return exc.errno in (errno.EPERM, errno.EACCES) or \ + getattr(exc, "winerror", -1) in (cext.ERROR_ACCESS_DENIED, + cext.ERROR_PRIVILEGE_NOT_HELD) + + +def convert_oserror(exc, pid=None, name=None): + """Convert OSError into NoSuchProcess or AccessDenied.""" + assert isinstance(exc, OSError), exc + if is_permission_err(exc): + return AccessDenied(pid=pid, name=name) + if exc.errno == errno.ESRCH: + return NoSuchProcess(pid=pid, name=name) + raise exc + + +def wrap_exceptions(fun): + """Decorator which converts OSError into NoSuchProcess or AccessDenied.""" + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + try: + return fun(self, *args, **kwargs) + except OSError as err: + raise convert_oserror(err, pid=self.pid, name=self._name) + return wrapper + + +def retry_error_partial_copy(fun): + """Workaround for https://github.com/giampaolo/psutil/issues/875. + See: https://stackoverflow.com/questions/4457745#4457745 + """ + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + delay = 0.0001 + times = 33 + for x in range(times): # retries for roughly 1 second + try: + return fun(self, *args, **kwargs) + except WindowsError as _: + err = _ + if err.winerror == ERROR_PARTIAL_COPY: + time.sleep(delay) + delay = min(delay * 2, 0.04) + continue + else: + raise + else: + msg = "%s retried %s times, converted to AccessDenied as it's " \ + "still returning %r" % (fun, times, err) + raise AccessDenied(pid=self.pid, name=self._name, msg=msg) + return wrapper + + +class Process(object): + """Wrapper class around underlying C implementation.""" + + __slots__ = ["pid", "_name", "_ppid", "_cache"] + + def __init__(self, pid): + self.pid = pid + self._name = None + self._ppid = None + + # --- oneshot() stuff + + def oneshot_enter(self): + self.oneshot_info.cache_activate(self) + + def oneshot_exit(self): + self.oneshot_info.cache_deactivate(self) + + @wrap_exceptions + @memoize_when_activated + def oneshot_info(self): + """Return multiple information about this process as a + raw tuple. + """ + ret = cext.proc_info(self.pid) + assert len(ret) == len(pinfo_map) + return ret + + @wrap_exceptions + def name(self): + """Return process name, which on Windows is always the final + part of the executable. + """ + # This is how PIDs 0 and 4 are always represented in taskmgr + # and process-hacker. + if self.pid == 0: + return "System Idle Process" + elif self.pid == 4: + return "System" + else: + try: + # Note: this will fail with AD for most PIDs owned + # by another user but it's faster. + return py2_strencode(os.path.basename(self.exe())) + except AccessDenied: + return py2_strencode(cext.proc_name(self.pid)) + + @wrap_exceptions + def exe(self): + # Dual implementation, see: + # https://github.com/giampaolo/psutil/pull/1413 + if not IS_WIN_XP: + exe = cext.proc_exe(self.pid) + else: + if self.pid in (0, 4): + # https://github.com/giampaolo/psutil/issues/414 + # https://github.com/giampaolo/psutil/issues/528 + raise AccessDenied(self.pid, self._name) + exe = cext.proc_exe(self.pid) + exe = convert_dos_path(exe) + return py2_strencode(exe) + + @wrap_exceptions + @retry_error_partial_copy + def cmdline(self): + if cext.WINVER >= cext.WINDOWS_8_1: + # PEB method detects cmdline changes but requires more + # privileges: https://github.com/giampaolo/psutil/pull/1398 + try: + ret = cext.proc_cmdline(self.pid, use_peb=True) + except OSError as err: + if is_permission_err(err): + ret = cext.proc_cmdline(self.pid, use_peb=False) + else: + raise + else: + ret = cext.proc_cmdline(self.pid, use_peb=True) + if PY3: + return ret + else: + return [py2_strencode(s) for s in ret] + + @wrap_exceptions + @retry_error_partial_copy + def environ(self): + ustr = cext.proc_environ(self.pid) + if ustr and not PY3: + assert isinstance(ustr, unicode), type(ustr) + return parse_environ_block(py2_strencode(ustr)) + + def ppid(self): + try: + return ppid_map()[self.pid] + except KeyError: + raise NoSuchProcess(self.pid, self._name) + + def _get_raw_meminfo(self): + try: + return cext.proc_memory_info(self.pid) + except OSError as err: + if is_permission_err(err): + # TODO: the C ext can probably be refactored in order + # to get this from cext.proc_info() + info = self.oneshot_info() + return ( + info[pinfo_map['num_page_faults']], + info[pinfo_map['peak_wset']], + info[pinfo_map['wset']], + info[pinfo_map['peak_paged_pool']], + info[pinfo_map['paged_pool']], + info[pinfo_map['peak_non_paged_pool']], + info[pinfo_map['non_paged_pool']], + info[pinfo_map['pagefile']], + info[pinfo_map['peak_pagefile']], + info[pinfo_map['mem_private']], + ) + raise + + @wrap_exceptions + def memory_info(self): + # on Windows RSS == WorkingSetSize and VSM == PagefileUsage. + # Underlying C function returns fields of PROCESS_MEMORY_COUNTERS + # struct. + t = self._get_raw_meminfo() + rss = t[2] # wset + vms = t[7] # pagefile + return pmem(*(rss, vms, ) + t) + + @wrap_exceptions + def memory_full_info(self): + basic_mem = self.memory_info() + uss = cext.proc_memory_uss(self.pid) + uss *= getpagesize() + return pfullmem(*basic_mem + (uss, )) + + def memory_maps(self): + try: + raw = cext.proc_memory_maps(self.pid) + except OSError as err: + # XXX - can't use wrap_exceptions decorator as we're + # returning a generator; probably needs refactoring. + raise convert_oserror(err, self.pid, self._name) + else: + for addr, perm, path, rss in raw: + path = convert_dos_path(path) + if not PY3: + assert isinstance(path, unicode), type(path) + path = py2_strencode(path) + addr = hex(addr) + yield (addr, perm, path, rss) + + @wrap_exceptions + def kill(self): + return cext.proc_kill(self.pid) + + @wrap_exceptions + def send_signal(self, sig): + os.kill(self.pid, sig) + + @wrap_exceptions + def wait(self, timeout=None): + if timeout is None: + cext_timeout = cext.INFINITE + else: + # WaitForSingleObject() expects time in milliseconds. + cext_timeout = int(timeout * 1000) + + timer = getattr(time, 'monotonic', time.time) + stop_at = timer() + timeout if timeout is not None else None + + try: + # Exit code is supposed to come from GetExitCodeProcess(). + # May also be None if OpenProcess() failed with + # ERROR_INVALID_PARAMETER, meaning PID is already gone. + exit_code = cext.proc_wait(self.pid, cext_timeout) + except cext.TimeoutExpired: + # WaitForSingleObject() returned WAIT_TIMEOUT. Just raise. + raise TimeoutExpired(timeout, self.pid, self._name) + except cext.TimeoutAbandoned: + # WaitForSingleObject() returned WAIT_ABANDONED, see: + # https://github.com/giampaolo/psutil/issues/1224 + # We'll just rely on the internal polling and return None + # when the PID disappears. Subprocess module does the same + # (return None): + # https://github.com/python/cpython/blob/ + # be50a7b627d0aa37e08fa8e2d5568891f19903ce/ + # Lib/subprocess.py#L1193-L1194 + exit_code = None + + # At this point WaitForSingleObject() returned WAIT_OBJECT_0, + # meaning the process is gone. Stupidly there are cases where + # its PID may still stick around so we do a further internal + # polling. + delay = 0.0001 + while True: + if not pid_exists(self.pid): + return exit_code + if stop_at and timer() >= stop_at: + raise TimeoutExpired(timeout, pid=self.pid, name=self._name) + time.sleep(delay) + delay = min(delay * 2, 0.04) # incremental delay + + @wrap_exceptions + def username(self): + if self.pid in (0, 4): + return 'NT AUTHORITY\\SYSTEM' + domain, user = cext.proc_username(self.pid) + return py2_strencode(domain) + '\\' + py2_strencode(user) + + @wrap_exceptions + def create_time(self): + # special case for kernel process PIDs; return system boot time + if self.pid in (0, 4): + return boot_time() + try: + return cext.proc_create_time(self.pid) + except OSError as err: + if is_permission_err(err): + return self.oneshot_info()[pinfo_map['create_time']] + raise + + @wrap_exceptions + def num_threads(self): + return self.oneshot_info()[pinfo_map['num_threads']] + + @wrap_exceptions + def threads(self): + rawlist = cext.proc_threads(self.pid) + retlist = [] + for thread_id, utime, stime in rawlist: + ntuple = _common.pthread(thread_id, utime, stime) + retlist.append(ntuple) + return retlist + + @wrap_exceptions + def cpu_times(self): + try: + user, system = cext.proc_cpu_times(self.pid) + except OSError as err: + if not is_permission_err(err): + raise + info = self.oneshot_info() + user = info[pinfo_map['user_time']] + system = info[pinfo_map['kernel_time']] + # Children user/system times are not retrievable (set to 0). + return _common.pcputimes(user, system, 0.0, 0.0) + + @wrap_exceptions + def suspend(self): + cext.proc_suspend_or_resume(self.pid, True) + + @wrap_exceptions + def resume(self): + cext.proc_suspend_or_resume(self.pid, False) + + @wrap_exceptions + @retry_error_partial_copy + def cwd(self): + if self.pid in (0, 4): + raise AccessDenied(self.pid, self._name) + # return a normalized pathname since the native C function appends + # "\\" at the and of the path + path = cext.proc_cwd(self.pid) + return py2_strencode(os.path.normpath(path)) + + @wrap_exceptions + def open_files(self): + if self.pid in (0, 4): + return [] + ret = set() + # Filenames come in in native format like: + # "\Device\HarddiskVolume1\Windows\systemew\file.txt" + # Convert the first part in the corresponding drive letter + # (e.g. "C:\") by using Windows's QueryDosDevice() + raw_file_names = cext.proc_open_files(self.pid) + for _file in raw_file_names: + _file = convert_dos_path(_file) + if isfile_strict(_file): + if not PY3: + _file = py2_strencode(_file) + ntuple = _common.popenfile(_file, -1) + ret.add(ntuple) + return list(ret) + + @wrap_exceptions + def connections(self, kind='inet'): + return net_connections(kind, _pid=self.pid) + + @wrap_exceptions + def nice_get(self): + value = cext.proc_priority_get(self.pid) + if enum is not None: + value = Priority(value) + return value + + @wrap_exceptions + def nice_set(self, value): + return cext.proc_priority_set(self.pid, value) + + # available on Windows >= Vista + if HAS_PROC_IO_PRIORITY: + @wrap_exceptions + def ionice_get(self): + ret = cext.proc_io_priority_get(self.pid) + if enum is not None: + ret = IOPriority(ret) + return ret + + @wrap_exceptions + def ionice_set(self, ioclass, value): + if value: + raise TypeError("value argument not accepted on Windows") + if ioclass not in (IOPRIO_VERYLOW, IOPRIO_LOW, IOPRIO_NORMAL, + IOPRIO_HIGH): + raise ValueError("%s is not a valid priority" % ioclass) + cext.proc_io_priority_set(self.pid, ioclass) + + @wrap_exceptions + def io_counters(self): + try: + ret = cext.proc_io_counters(self.pid) + except OSError as err: + if not is_permission_err(err): + raise + info = self.oneshot_info() + ret = ( + info[pinfo_map['io_rcount']], + info[pinfo_map['io_wcount']], + info[pinfo_map['io_rbytes']], + info[pinfo_map['io_wbytes']], + info[pinfo_map['io_count_others']], + info[pinfo_map['io_bytes_others']], + ) + return pio(*ret) + + @wrap_exceptions + def status(self): + suspended = cext.proc_is_suspended(self.pid) + if suspended: + return _common.STATUS_STOPPED + else: + return _common.STATUS_RUNNING + + @wrap_exceptions + def cpu_affinity_get(self): + def from_bitmask(x): + return [i for i in xrange(64) if (1 << i) & x] + bitmask = cext.proc_cpu_affinity_get(self.pid) + return from_bitmask(bitmask) + + @wrap_exceptions + def cpu_affinity_set(self, value): + def to_bitmask(l): + if not l: + raise ValueError("invalid argument %r" % l) + out = 0 + for b in l: + out |= 2 ** b + return out + + # SetProcessAffinityMask() states that ERROR_INVALID_PARAMETER + # is returned for an invalid CPU but this seems not to be true, + # therefore we check CPUs validy beforehand. + allcpus = list(range(len(per_cpu_times()))) + for cpu in value: + if cpu not in allcpus: + if not isinstance(cpu, (int, long)): + raise TypeError( + "invalid CPU %r; an integer is required" % cpu) + else: + raise ValueError("invalid CPU %r" % cpu) + + bitmask = to_bitmask(value) + cext.proc_cpu_affinity_set(self.pid, bitmask) + + @wrap_exceptions + def num_handles(self): + try: + return cext.proc_num_handles(self.pid) + except OSError as err: + if is_permission_err(err): + return self.oneshot_info()[pinfo_map['num_handles']] + raise + + @wrap_exceptions + def num_ctx_switches(self): + ctx_switches = self.oneshot_info()[pinfo_map['ctx_switches']] + # only voluntary ctx switches are supported + return _common.pctxsw(ctx_switches, 0) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/setup.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/setup.py new file mode 100644 index 000000000..21de23548 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/psutil/setup.py @@ -0,0 +1,227 @@ +__all__ = ["get_extensions"] + +import contextlib +import io +import os +import platform +from setuptools import Extension +import shutil +import sys +import tempfile + +POSIX = os.name == "posix" +WINDOWS = os.name == "nt" +LINUX = sys.platform.startswith("linux") +MACOS = sys.platform.startswith("darwin") +OSX = MACOS # deprecated alias +FREEBSD = sys.platform.startswith("freebsd") +OPENBSD = sys.platform.startswith("openbsd") +NETBSD = sys.platform.startswith("netbsd") +BSD = FREEBSD or OPENBSD or NETBSD +SUNOS = sys.platform.startswith(("sunos", "solaris")) +AIX = sys.platform.startswith("aix") + + +@contextlib.contextmanager +def silenced_output(stream_name): + class DummyFile(io.BytesIO): + # see: https://github.com/giampaolo/psutil/issues/678 + errors = "ignore" + + def write(self, s): + pass + + orig = getattr(sys, stream_name) + try: + setattr(sys, stream_name, DummyFile()) + yield + finally: + setattr(sys, stream_name, orig) + + +def get_extensions(): + macros = [("PSUTIL_VERSION", 567)] + if POSIX: + macros.append(("PSUTIL_POSIX", 1)) + if BSD: + macros.append(("PSUTIL_BSD", 1)) + + sources = ["ddtrace/vendor/psutil/_psutil_common.c"] + if POSIX: + sources.append("ddtrace/vendor/psutil/_psutil_posix.c") + + if WINDOWS: + + def get_winver(): + win_maj, win_min = sys.getwindowsversion()[0:2] + return "0x0%s" % ((win_maj * 100) + win_min) + + if sys.getwindowsversion()[0] < 6: + msg = "this Windows version is too old (< Windows Vista); " + msg += "psutil 3.4.2 is the latest version which supports Windows " + msg += "2000, XP and 2003 server" + raise RuntimeError(msg) + + macros.append(("PSUTIL_WINDOWS", 1)) + macros.extend( + [ + # be nice to mingw, see: + # http://www.mingw.org/wiki/Use_more_recent_defined_functions + ("_WIN32_WINNT", get_winver()), + ("_AVAIL_WINVER_", get_winver()), + ("_CRT_SECURE_NO_WARNINGS", None), + # see: https://github.com/giampaolo/psutil/issues/348 + ("PSAPI_VERSION", 1), + ] + ) + + sources += [ + "ddtrace/vendor/psutil/_psutil_windows.c", + "ddtrace/vendor/psutil/arch/windows/process_info.c", + "ddtrace/vendor/psutil/arch/windows/process_handles.c", + "ddtrace/vendor/psutil/arch/windows/security.c", + "ddtrace/vendor/psutil/arch/windows/inet_ntop.c", + "ddtrace/vendor/psutil/arch/windows/services.c", + "ddtrace/vendor/psutil/arch/windows/global.c", + "ddtrace/vendor/psutil/arch/windows/wmi.c", + ] + ext = Extension( + "ddtrace.vendor.psutil._psutil_windows", + sources=sources, + define_macros=macros, + libraries=[ + "psapi", + "kernel32", + "advapi32", + "shell32", + "netapi32", + "wtsapi32", + "ws2_32", + "PowrProf", + "pdh", + ], + # extra_compile_args=["/Z7"], + # extra_link_args=["/DEBUG"] + ) + + elif MACOS: + macros.append(("PSUTIL_OSX", 1)) + sources += [ + "ddtrace/vendor/psutil/_psutil_osx.c", + "ddtrace/vendor/psutil/arch/osx/process_info.c", + ] + ext = Extension( + "ddtrace.vendor.psutil._psutil_osx", + sources=sources, + define_macros=macros, + extra_link_args=["-framework", "CoreFoundation", "-framework", "IOKit"], + ) + + elif FREEBSD: + macros.append(("PSUTIL_FREEBSD", 1)) + sources += [ + "ddtrace/vendor/psutil/_psutil_bsd.c", + "ddtrace/vendor/psutil/arch/freebsd/specific.c", + "ddtrace/vendor/psutil/arch/freebsd/sys_socks.c", + "ddtrace/vendor/psutil/arch/freebsd/proc_socks.c", + ] + ext = Extension( + "ddtrace.vendor.psutil._psutil_bsd", sources=sources, define_macros=macros, libraries=["devstat"], + ) + + elif OPENBSD: + macros.append(("PSUTIL_OPENBSD", 1)) + ext = Extension( + "ddtrace.vendor.psutil._psutil_bsd", + sources=sources + ["ddtrace/vendor/psutil/_psutil_bsd.c", "ddtrace/vendor/psutil/arch/openbsd/specific.c"], + define_macros=macros, + libraries=["kvm"], + ) + + elif NETBSD: + macros.append(("PSUTIL_NETBSD", 1)) + sources += [ + "ddtrace/vendor/psutil/_psutil_bsd.c", + "ddtrace/vendor/psutil/arch/netbsd/specific.c", + "ddtrace/vendor/psutil/arch/netbsd/socks.c", + ] + ext = Extension("ddtrace.vendor.psutil._psutil_bsd", sources=sources, define_macros=macros, libraries=["kvm"],) + + elif LINUX: + + def get_ethtool_macro(): + # see: https://github.com/giampaolo/ddtrace/vendor/psutil/issues/659 + from distutils.unixccompiler import UnixCCompiler + from distutils.errors import CompileError + + with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="wt") as f: + f.write("#include ") + + output_dir = tempfile.mkdtemp() + try: + compiler = UnixCCompiler() + # https://github.com/giampaolo/ddtrace/vendor/psutil/pull/1568 + if os.getenv("CC"): + compiler.set_executable("compiler_so", os.getenv("CC")) + with silenced_output("stderr"): + with silenced_output("stdout"): + compiler.compile([f.name], output_dir=output_dir) + except CompileError: + return ("PSUTIL_ETHTOOL_MISSING_TYPES", 1) + else: + return None + finally: + os.remove(f.name) + shutil.rmtree(output_dir) + + macros.append(("PSUTIL_LINUX", 1)) + ETHTOOL_MACRO = get_ethtool_macro() + if ETHTOOL_MACRO is not None: + macros.append(ETHTOOL_MACRO) + ext = Extension( + "ddtrace.vendor.psutil._psutil_linux", + sources=sources + ["ddtrace/vendor/psutil/_psutil_linux.c"], + define_macros=macros, + ) + + elif SUNOS: + macros.append(("PSUTIL_SUNOS", 1)) + sources += [ + "ddtrace/vendor/psutil/_psutil_sunos.c", + "ddtrace/vendor/psutil/arch/solaris/v10/ifaddrs.c", + "ddtrace/vendor/psutil/arch/solaris/environ.c", + ] + ext = Extension( + "ddtrace.vendor.psutil._psutil_sunos", + sources=sources, + define_macros=macros, + libraries=["kstat", "nsl", "socket"], + ) + + elif AIX: + macros.append(("PSUTIL_AIX", 1)) + sources += [ + "ddtrace/vendor/psutil/_psutil_aix.c", + "ddtrace/vendor/psutil/arch/aix/net_connections.c", + "ddtrace/vendor/psutil/arch/aix/common.c", + "ddtrace/vendor/psutil/arch/aix/ifaddrs.c", + ] + ext = Extension( + "ddtrace.vendor.psutil._psutil_aix", sources=sources, libraries=["perfstat"], define_macros=macros, + ) + else: + raise RuntimeError("platform %s is not supported" % sys.platform) + + if POSIX: + posix_extension = Extension("ddtrace.vendor.psutil._psutil_posix", define_macros=macros, sources=sources) + if SUNOS: + posix_extension.libraries.append("socket") + if platform.release() == "5.10": + posix_extension.sources.append("ddtrace/vendor/psutil/arch/solaris/v10/ifaddrs.c") + posix_extension.define_macros.append(("PSUTIL_SUNOS10", 1)) + elif AIX: + posix_extension.sources.append("ddtrace/vendor/psutil/arch/aix/ifaddrs.c") + + return [ext, posix_extension] + else: + return [ext] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__init__.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__init__.py new file mode 100644 index 000000000..7be739bf6 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__init__.py @@ -0,0 +1,16 @@ +__version_info__ = ('1', '12', '1') +__version__ = '.'.join(__version_info__) + +from .wrappers import (ObjectProxy, CallableObjectProxy, FunctionWrapper, + BoundFunctionWrapper, WeakFunctionProxy, PartialCallableObjectProxy, + resolve_path, apply_patch, wrap_object, wrap_object_attribute, + function_wrapper, wrap_function_wrapper, patch_function_wrapper, + transient_function_wrapper) + +from .decorators import (adapter_factory, AdapterFactory, decorator, + synchronized) + +from .importer import (register_post_import_hook, when_imported, + notify_module_loaded, discover_post_import_hooks) + +from inspect import getcallargs diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..c396032c6 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__pycache__/decorators.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__pycache__/decorators.cpython-38.pyc new file mode 100644 index 000000000..6920f40ac Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__pycache__/decorators.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__pycache__/importer.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__pycache__/importer.cpython-38.pyc new file mode 100644 index 000000000..46324eb6e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__pycache__/importer.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__pycache__/setup.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__pycache__/setup.cpython-38.pyc new file mode 100644 index 000000000..87e9984b4 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__pycache__/setup.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__pycache__/wrappers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__pycache__/wrappers.cpython-38.pyc new file mode 100644 index 000000000..83452e58c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/__pycache__/wrappers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/_wrappers.cpython-38-darwin.so b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/_wrappers.cpython-38-darwin.so new file mode 100755 index 000000000..c27a5af2d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/_wrappers.cpython-38-darwin.so differ diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/decorators.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/decorators.py new file mode 100644 index 000000000..506303d7a --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/decorators.py @@ -0,0 +1,516 @@ +"""This module implements decorators for implementing other decorators +as well as some commonly used decorators. + +""" + +import sys + +PY2 = sys.version_info[0] == 2 + +if PY2: + string_types = basestring, + + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + +else: + string_types = str, + + import builtins + + exec_ = getattr(builtins, "exec") + del builtins + +from functools import partial +from inspect import ismethod, isclass, formatargspec +from collections import namedtuple +from threading import Lock, RLock + +try: + from inspect import signature +except ImportError: + pass + +from .wrappers import (FunctionWrapper, BoundFunctionWrapper, ObjectProxy, + CallableObjectProxy) + +# Adapter wrapper for the wrapped function which will overlay certain +# properties from the adapter function onto the wrapped function so that +# functions such as inspect.getargspec(), inspect.getfullargspec(), +# inspect.signature() and inspect.getsource() return the correct results +# one would expect. + +class _AdapterFunctionCode(CallableObjectProxy): + + def __init__(self, wrapped_code, adapter_code): + super(_AdapterFunctionCode, self).__init__(wrapped_code) + self._self_adapter_code = adapter_code + + @property + def co_argcount(self): + return self._self_adapter_code.co_argcount + + @property + def co_code(self): + return self._self_adapter_code.co_code + + @property + def co_flags(self): + return self._self_adapter_code.co_flags + + @property + def co_kwonlyargcount(self): + return self._self_adapter_code.co_kwonlyargcount + + @property + def co_varnames(self): + return self._self_adapter_code.co_varnames + +class _AdapterFunctionSurrogate(CallableObjectProxy): + + def __init__(self, wrapped, adapter): + super(_AdapterFunctionSurrogate, self).__init__(wrapped) + self._self_adapter = adapter + + @property + def __code__(self): + return _AdapterFunctionCode(self.__wrapped__.__code__, + self._self_adapter.__code__) + + @property + def __defaults__(self): + return self._self_adapter.__defaults__ + + @property + def __kwdefaults__(self): + return self._self_adapter.__kwdefaults__ + + @property + def __signature__(self): + if 'signature' not in globals(): + return self._self_adapter.__signature__ + else: + return signature(self._self_adapter) + + if PY2: + func_code = __code__ + func_defaults = __defaults__ + +class _BoundAdapterWrapper(BoundFunctionWrapper): + + @property + def __func__(self): + return _AdapterFunctionSurrogate(self.__wrapped__.__func__, + self._self_parent._self_adapter) + + @property + def __signature__(self): + if 'signature' not in globals(): + return self.__wrapped__.__signature__ + else: + return signature(self._self_parent._self_adapter) + + if PY2: + im_func = __func__ + +class AdapterWrapper(FunctionWrapper): + + __bound_function_wrapper__ = _BoundAdapterWrapper + + def __init__(self, *args, **kwargs): + adapter = kwargs.pop('adapter') + super(AdapterWrapper, self).__init__(*args, **kwargs) + self._self_surrogate = _AdapterFunctionSurrogate( + self.__wrapped__, adapter) + self._self_adapter = adapter + + @property + def __code__(self): + return self._self_surrogate.__code__ + + @property + def __defaults__(self): + return self._self_surrogate.__defaults__ + + @property + def __kwdefaults__(self): + return self._self_surrogate.__kwdefaults__ + + if PY2: + func_code = __code__ + func_defaults = __defaults__ + + @property + def __signature__(self): + return self._self_surrogate.__signature__ + +class AdapterFactory(object): + def __call__(self, wrapped): + raise NotImplementedError() + +class DelegatedAdapterFactory(AdapterFactory): + def __init__(self, factory): + super(DelegatedAdapterFactory, self).__init__() + self.factory = factory + def __call__(self, wrapped): + return self.factory(wrapped) + +adapter_factory = DelegatedAdapterFactory + +# Decorator for creating other decorators. This decorator and the +# wrappers which they use are designed to properly preserve any name +# attributes, function signatures etc, in addition to the wrappers +# themselves acting like a transparent proxy for the original wrapped +# function so the wrapper is effectively indistinguishable from the +# original wrapped function. + +def decorator(wrapper=None, enabled=None, adapter=None): + # The decorator should be supplied with a single positional argument + # which is the wrapper function to be used to implement the + # decorator. This may be preceded by a step whereby the keyword + # arguments are supplied to customise the behaviour of the + # decorator. The 'adapter' argument is used to optionally denote a + # separate function which is notionally used by an adapter + # decorator. In that case parts of the function '__code__' and + # '__defaults__' attributes are used from the adapter function + # rather than those of the wrapped function. This allows for the + # argument specification from inspect.getargspec() and similar + # functions to be overridden with a prototype for a different + # function than what was wrapped. The 'enabled' argument provides a + # way to enable/disable the use of the decorator. If the type of + # 'enabled' is a boolean, then it is evaluated immediately and the + # wrapper not even applied if it is False. If not a boolean, it will + # be evaluated when the wrapper is called for an unbound wrapper, + # and when binding occurs for a bound wrapper. When being evaluated, + # if 'enabled' is callable it will be called to obtain the value to + # be checked. If False, the wrapper will not be called and instead + # the original wrapped function will be called directly instead. + + if wrapper is not None: + # Helper function for creating wrapper of the appropriate + # time when we need it down below. + + def _build(wrapped, wrapper, enabled=None, adapter=None): + if adapter: + if isinstance(adapter, AdapterFactory): + adapter = adapter(wrapped) + + if not callable(adapter): + ns = {} + if not isinstance(adapter, string_types): + adapter = formatargspec(*adapter) + exec_('def adapter{}: pass'.format(adapter), ns, ns) + adapter = ns['adapter'] + + return AdapterWrapper(wrapped=wrapped, wrapper=wrapper, + enabled=enabled, adapter=adapter) + + return FunctionWrapper(wrapped=wrapped, wrapper=wrapper, + enabled=enabled) + + # The wrapper has been provided so return the final decorator. + # The decorator is itself one of our function wrappers so we + # can determine when it is applied to functions, instance methods + # or class methods. This allows us to bind the instance or class + # method so the appropriate self or cls attribute is supplied + # when it is finally called. + + def _wrapper(wrapped, instance, args, kwargs): + # We first check for the case where the decorator was applied + # to a class type. + # + # @decorator + # class mydecoratorclass(object): + # def __init__(self, arg=None): + # self.arg = arg + # def __call__(self, wrapped, instance, args, kwargs): + # return wrapped(*args, **kwargs) + # + # @mydecoratorclass(arg=1) + # def function(): + # pass + # + # In this case an instance of the class is to be used as the + # decorator wrapper function. If args was empty at this point, + # then it means that there were optional keyword arguments + # supplied to be used when creating an instance of the class + # to be used as the wrapper function. + + if instance is None and isclass(wrapped) and not args: + # We still need to be passed the target function to be + # wrapped as yet, so we need to return a further function + # to be able to capture it. + + def _capture(target_wrapped): + # Now have the target function to be wrapped and need + # to create an instance of the class which is to act + # as the decorator wrapper function. Before we do that, + # we need to first check that use of the decorator + # hadn't been disabled by a simple boolean. If it was, + # the target function to be wrapped is returned instead. + + _enabled = enabled + if type(_enabled) is bool: + if not _enabled: + return target_wrapped + _enabled = None + + # Now create an instance of the class which is to act + # as the decorator wrapper function. Any arguments had + # to be supplied as keyword only arguments so that is + # all we pass when creating it. + + target_wrapper = wrapped(**kwargs) + + # Finally build the wrapper itself and return it. + + return _build(target_wrapped, target_wrapper, + _enabled, adapter) + + return _capture + + # We should always have the target function to be wrapped at + # this point as the first (and only) value in args. + + target_wrapped = args[0] + + # Need to now check that use of the decorator hadn't been + # disabled by a simple boolean. If it was, then target + # function to be wrapped is returned instead. + + _enabled = enabled + if type(_enabled) is bool: + if not _enabled: + return target_wrapped + _enabled = None + + # We now need to build the wrapper, but there are a couple of + # different cases we need to consider. + + if instance is None: + if isclass(wrapped): + # In this case the decorator was applied to a class + # type but optional keyword arguments were not supplied + # for initialising an instance of the class to be used + # as the decorator wrapper function. + # + # @decorator + # class mydecoratorclass(object): + # def __init__(self, arg=None): + # self.arg = arg + # def __call__(self, wrapped, instance, + # args, kwargs): + # return wrapped(*args, **kwargs) + # + # @mydecoratorclass + # def function(): + # pass + # + # We still need to create an instance of the class to + # be used as the decorator wrapper function, but no + # arguments are pass. + + target_wrapper = wrapped() + + else: + # In this case the decorator was applied to a normal + # function, or possibly a static method of a class. + # + # @decorator + # def mydecoratorfuntion(wrapped, instance, + # args, kwargs): + # return wrapped(*args, **kwargs) + # + # @mydecoratorfunction + # def function(): + # pass + # + # That normal function becomes the decorator wrapper + # function. + + target_wrapper = wrapper + + else: + if isclass(instance): + # In this case the decorator was applied to a class + # method. + # + # class myclass(object): + # @decorator + # @classmethod + # def decoratorclassmethod(cls, wrapped, + # instance, args, kwargs): + # return wrapped(*args, **kwargs) + # + # instance = myclass() + # + # @instance.decoratorclassmethod + # def function(): + # pass + # + # This one is a bit strange because binding was actually + # performed on the wrapper created by our decorator + # factory. We need to apply that binding to the decorator + # wrapper function which which the decorator factory + # was applied to. + + target_wrapper = wrapper.__get__(None, instance) + + else: + # In this case the decorator was applied to an instance + # method. + # + # class myclass(object): + # @decorator + # def decoratorclassmethod(self, wrapped, + # instance, args, kwargs): + # return wrapped(*args, **kwargs) + # + # instance = myclass() + # + # @instance.decoratorclassmethod + # def function(): + # pass + # + # This one is a bit strange because binding was actually + # performed on the wrapper created by our decorator + # factory. We need to apply that binding to the decorator + # wrapper function which which the decorator factory + # was applied to. + + target_wrapper = wrapper.__get__(instance, type(instance)) + + # Finally build the wrapper itself and return it. + + return _build(target_wrapped, target_wrapper, _enabled, adapter) + + # We first return our magic function wrapper here so we can + # determine in what context the decorator factory was used. In + # other words, it is itself a universal decorator. The decorator + # function is used as the adapter so that linters see a signature + # corresponding to the decorator and not the wrapper it is being + # applied to. + + return _build(wrapper, _wrapper, adapter=decorator) + + else: + # The wrapper still has not been provided, so we are just + # collecting the optional keyword arguments. Return the + # decorator again wrapped in a partial using the collected + # arguments. + + return partial(decorator, enabled=enabled, adapter=adapter) + +# Decorator for implementing thread synchronization. It can be used as a +# decorator, in which case the synchronization context is determined by +# what type of function is wrapped, or it can also be used as a context +# manager, where the user needs to supply the correct synchronization +# context. It is also possible to supply an object which appears to be a +# synchronization primitive of some sort, by virtue of having release() +# and acquire() methods. In that case that will be used directly as the +# synchronization primitive without creating a separate lock against the +# derived or supplied context. + +def synchronized(wrapped): + # Determine if being passed an object which is a synchronization + # primitive. We can't check by type for Lock, RLock, Semaphore etc, + # as the means of creating them isn't the type. Therefore use the + # existence of acquire() and release() methods. This is more + # extensible anyway as it allows custom synchronization mechanisms. + + if hasattr(wrapped, 'acquire') and hasattr(wrapped, 'release'): + # We remember what the original lock is and then return a new + # decorator which accesses and locks it. When returning the new + # decorator we wrap it with an object proxy so we can override + # the context manager methods in case it is being used to wrap + # synchronized statements with a 'with' statement. + + lock = wrapped + + @decorator + def _synchronized(wrapped, instance, args, kwargs): + # Execute the wrapped function while the original supplied + # lock is held. + + with lock: + return wrapped(*args, **kwargs) + + class _PartialDecorator(CallableObjectProxy): + + def __enter__(self): + lock.acquire() + return lock + + def __exit__(self, *args): + lock.release() + + return _PartialDecorator(wrapped=_synchronized) + + # Following only apply when the lock is being created automatically + # based on the context of what was supplied. In this case we supply + # a final decorator, but need to use FunctionWrapper directly as we + # want to derive from it to add context manager methods in case it is + # being used to wrap synchronized statements with a 'with' statement. + + def _synchronized_lock(context): + # Attempt to retrieve the lock for the specific context. + + lock = vars(context).get('_synchronized_lock', None) + + if lock is None: + # There is no existing lock defined for the context we + # are dealing with so we need to create one. This needs + # to be done in a way to guarantee there is only one + # created, even if multiple threads try and create it at + # the same time. We can't always use the setdefault() + # method on the __dict__ for the context. This is the + # case where the context is a class, as __dict__ is + # actually a dictproxy. What we therefore do is use a + # meta lock on this wrapper itself, to control the + # creation and assignment of the lock attribute against + # the context. + + with synchronized._synchronized_meta_lock: + # We need to check again for whether the lock we want + # exists in case two threads were trying to create it + # at the same time and were competing to create the + # meta lock. + + lock = vars(context).get('_synchronized_lock', None) + + if lock is None: + lock = RLock() + setattr(context, '_synchronized_lock', lock) + + return lock + + def _synchronized_wrapper(wrapped, instance, args, kwargs): + # Execute the wrapped function while the lock for the + # desired context is held. If instance is None then the + # wrapped function is used as the context. + + with _synchronized_lock(instance if instance is not None else wrapped): + return wrapped(*args, **kwargs) + + class _FinalDecorator(FunctionWrapper): + + def __enter__(self): + self._self_lock = _synchronized_lock(self.__wrapped__) + self._self_lock.acquire() + return self._self_lock + + def __exit__(self, *args): + self._self_lock.release() + + return _FinalDecorator(wrapped=wrapped, wrapper=_synchronized_wrapper) + +synchronized._synchronized_meta_lock = Lock() diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/importer.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/importer.py new file mode 100644 index 000000000..4665f3865 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/importer.py @@ -0,0 +1,230 @@ +"""This module implements a post import hook mechanism styled after what is +described in PEP-369. Note that it doesn't cope with modules being reloaded. + +""" + +import sys +import threading + +PY2 = sys.version_info[0] == 2 + +if PY2: + string_types = basestring, +else: + import importlib + string_types = str, + +from .decorators import synchronized + +# The dictionary registering any post import hooks to be triggered once +# the target module has been imported. Once a module has been imported +# and the hooks fired, the list of hooks recorded against the target +# module will be truncacted but the list left in the dictionary. This +# acts as a flag to indicate that the module had already been imported. + +_post_import_hooks = {} +_post_import_hooks_init = False +_post_import_hooks_lock = threading.RLock() + +# Register a new post import hook for the target module name. This +# differs from the PEP-369 implementation in that it also allows the +# hook function to be specified as a string consisting of the name of +# the callback in the form 'module:function'. This will result in a +# proxy callback being registered which will defer loading of the +# specified module containing the callback function until required. + +def _create_import_hook_from_string(name): + def import_hook(module): + module_name, function = name.split(':') + attrs = function.split('.') + __import__(module_name) + callback = sys.modules[module_name] + for attr in attrs: + callback = getattr(callback, attr) + return callback(module) + return import_hook + +@synchronized(_post_import_hooks_lock) +def register_post_import_hook(hook, name): + # Create a deferred import hook if hook is a string name rather than + # a callable function. + + if isinstance(hook, string_types): + hook = _create_import_hook_from_string(hook) + + # Automatically install the import hook finder if it has not already + # been installed. + + global _post_import_hooks_init + + if not _post_import_hooks_init: + _post_import_hooks_init = True + sys.meta_path.insert(0, ImportHookFinder()) + + # Determine if any prior registration of a post import hook for + # the target modules has occurred and act appropriately. + + hooks = _post_import_hooks.get(name, None) + + if hooks is None: + # No prior registration of post import hooks for the target + # module. We need to check whether the module has already been + # imported. If it has we fire the hook immediately and add an + # empty list to the registry to indicate that the module has + # already been imported and hooks have fired. Otherwise add + # the post import hook to the registry. + + module = sys.modules.get(name, None) + + if module is not None: + _post_import_hooks[name] = [] + hook(module) + + else: + _post_import_hooks[name] = [hook] + + elif hooks == []: + # A prior registration of port import hooks for the target + # module was done and the hooks already fired. Fire the hook + # immediately. + + module = sys.modules[name] + hook(module) + + else: + # A prior registration of port import hooks for the target + # module was done but the module has not yet been imported. + + _post_import_hooks[name].append(hook) + +# Register post import hooks defined as package entry points. + +def _create_import_hook_from_entrypoint(entrypoint): + def import_hook(module): + __import__(entrypoint.module_name) + callback = sys.modules[entrypoint.module_name] + for attr in entrypoint.attrs: + callback = getattr(callback, attr) + return callback(module) + return import_hook + +def discover_post_import_hooks(group): + try: + import pkg_resources + except ImportError: + return + + for entrypoint in pkg_resources.iter_entry_points(group=group): + callback = _create_import_hook_from_entrypoint(entrypoint) + register_post_import_hook(callback, entrypoint.name) + +# Indicate that a module has been loaded. Any post import hooks which +# were registered against the target module will be invoked. If an +# exception is raised in any of the post import hooks, that will cause +# the import of the target module to fail. + +@synchronized(_post_import_hooks_lock) +def notify_module_loaded(module): + name = getattr(module, '__name__', None) + hooks = _post_import_hooks.get(name, None) + + if hooks: + _post_import_hooks[name] = [] + + for hook in hooks: + hook(module) + +# A custom module import finder. This intercepts attempts to import +# modules and watches out for attempts to import target modules of +# interest. When a module of interest is imported, then any post import +# hooks which are registered will be invoked. + +class _ImportHookLoader: + + def load_module(self, fullname): + module = sys.modules[fullname] + notify_module_loaded(module) + + return module + +class _ImportHookChainedLoader: + + def __init__(self, loader): + self.loader = loader + + def load_module(self, fullname): + module = self.loader.load_module(fullname) + notify_module_loaded(module) + + return module + +class ImportHookFinder: + + def __init__(self): + self.in_progress = {} + + @synchronized(_post_import_hooks_lock) + def find_module(self, fullname, path=None): + # If the module being imported is not one we have registered + # post import hooks for, we can return immediately. We will + # take no further part in the importing of this module. + + if not fullname in _post_import_hooks: + return None + + # When we are interested in a specific module, we will call back + # into the import system a second time to defer to the import + # finder that is supposed to handle the importing of the module. + # We set an in progress flag for the target module so that on + # the second time through we don't trigger another call back + # into the import system and cause a infinite loop. + + if fullname in self.in_progress: + return None + + self.in_progress[fullname] = True + + # Now call back into the import system again. + + try: + if PY2: + # For Python 2 we don't have much choice but to + # call back in to __import__(). This will + # actually cause the module to be imported. If no + # module could be found then ImportError will be + # raised. Otherwise we return a loader which + # returns the already loaded module and invokes + # the post import hooks. + + __import__(fullname) + + return _ImportHookLoader() + + else: + # For Python 3 we need to use find_spec().loader + # from the importlib.util module. It doesn't actually + # import the target module and only finds the + # loader. If a loader is found, we need to return + # our own loader which will then in turn call the + # real loader to import the module and invoke the + # post import hooks. + try: + import importlib.util + loader = importlib.util.find_spec(fullname).loader + except (ImportError, AttributeError): + loader = importlib.find_loader(fullname, path) + if loader: + return _ImportHookChainedLoader(loader) + + + finally: + del self.in_progress[fullname] + +# Decorator for marking that a function should be called as a post +# import hook when the target module is imported. + +def when_imported(name): + def register(hook): + register_post_import_hook(hook, name) + return hook + return register diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/setup.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/setup.py new file mode 100644 index 000000000..dae324bd2 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/setup.py @@ -0,0 +1,7 @@ +__all__ = ["get_extensions"] + +from setuptools import Extension + + +def get_extensions(): + return [Extension("ddtrace.vendor.wrapt._wrappers", sources=["ddtrace/vendor/wrapt/_wrappers.c"],)] diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/wrappers.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/wrappers.py new file mode 100644 index 000000000..18cf5e053 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/vendor/wrapt/wrappers.py @@ -0,0 +1,947 @@ +import os +import sys +import functools +import operator +import weakref +import inspect + +PY2 = sys.version_info[0] == 2 + +if PY2: + string_types = basestring, +else: + string_types = str, + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + return meta("NewBase", bases, {}) + +class _ObjectProxyMethods(object): + + # We use properties to override the values of __module__ and + # __doc__. If we add these in ObjectProxy, the derived class + # __dict__ will still be setup to have string variants of these + # attributes and the rules of descriptors means that they appear to + # take precedence over the properties in the base class. To avoid + # that, we copy the properties into the derived class type itself + # via a meta class. In that way the properties will always take + # precedence. + + @property + def __module__(self): + return self.__wrapped__.__module__ + + @__module__.setter + def __module__(self, value): + self.__wrapped__.__module__ = value + + @property + def __doc__(self): + return self.__wrapped__.__doc__ + + @__doc__.setter + def __doc__(self, value): + self.__wrapped__.__doc__ = value + + # We similar use a property for __dict__. We need __dict__ to be + # explicit to ensure that vars() works as expected. + + @property + def __dict__(self): + return self.__wrapped__.__dict__ + + # Need to also propagate the special __weakref__ attribute for case + # where decorating classes which will define this. If do not define + # it and use a function like inspect.getmembers() on a decorator + # class it will fail. This can't be in the derived classes. + + @property + def __weakref__(self): + return self.__wrapped__.__weakref__ + +class _ObjectProxyMetaType(type): + def __new__(cls, name, bases, dictionary): + # Copy our special properties into the class so that they + # always take precedence over attributes of the same name added + # during construction of a derived class. This is to save + # duplicating the implementation for them in all derived classes. + + dictionary.update(vars(_ObjectProxyMethods)) + + return type.__new__(cls, name, bases, dictionary) + +class ObjectProxy(with_metaclass(_ObjectProxyMetaType)): + + __slots__ = '__wrapped__' + + def __init__(self, wrapped): + object.__setattr__(self, '__wrapped__', wrapped) + + # Python 3.2+ has the __qualname__ attribute, but it does not + # allow it to be overridden using a property and it must instead + # be an actual string object instead. + + try: + object.__setattr__(self, '__qualname__', wrapped.__qualname__) + except AttributeError: + pass + + @property + def __name__(self): + return self.__wrapped__.__name__ + + @__name__.setter + def __name__(self, value): + self.__wrapped__.__name__ = value + + @property + def __class__(self): + return self.__wrapped__.__class__ + + @__class__.setter + def __class__(self, value): + self.__wrapped__.__class__ = value + + @property + def __annotations__(self): + return self.__wrapped__.__annotations__ + + @__annotations__.setter + def __annotations__(self, value): + self.__wrapped__.__annotations__ = value + + def __dir__(self): + return dir(self.__wrapped__) + + def __str__(self): + return str(self.__wrapped__) + + if not PY2: + def __bytes__(self): + return bytes(self.__wrapped__) + + def __repr__(self): + return '<{} at 0x{:x} for {} at 0x{:x}>'.format( + type(self).__name__, id(self), + type(self.__wrapped__).__name__, + id(self.__wrapped__)) + + def __reversed__(self): + return reversed(self.__wrapped__) + + if not PY2: + def __round__(self): + return round(self.__wrapped__) + + if sys.hexversion >= 0x03070000: + def __mro_entries__(self, bases): + return (self.__wrapped__,) + + def __lt__(self, other): + return self.__wrapped__ < other + + def __le__(self, other): + return self.__wrapped__ <= other + + def __eq__(self, other): + return self.__wrapped__ == other + + def __ne__(self, other): + return self.__wrapped__ != other + + def __gt__(self, other): + return self.__wrapped__ > other + + def __ge__(self, other): + return self.__wrapped__ >= other + + def __hash__(self): + return hash(self.__wrapped__) + + def __nonzero__(self): + return bool(self.__wrapped__) + + def __bool__(self): + return bool(self.__wrapped__) + + def __setattr__(self, name, value): + if name.startswith('_self_'): + object.__setattr__(self, name, value) + + elif name == '__wrapped__': + object.__setattr__(self, name, value) + try: + object.__delattr__(self, '__qualname__') + except AttributeError: + pass + try: + object.__setattr__(self, '__qualname__', value.__qualname__) + except AttributeError: + pass + + elif name == '__qualname__': + setattr(self.__wrapped__, name, value) + object.__setattr__(self, name, value) + + elif hasattr(type(self), name): + object.__setattr__(self, name, value) + + else: + setattr(self.__wrapped__, name, value) + + def __getattr__(self, name): + # If we are being to lookup '__wrapped__' then the + # '__init__()' method cannot have been called. + + if name == '__wrapped__': + raise ValueError('wrapper has not been initialised') + + return getattr(self.__wrapped__, name) + + def __delattr__(self, name): + if name.startswith('_self_'): + object.__delattr__(self, name) + + elif name == '__wrapped__': + raise TypeError('__wrapped__ must be an object') + + elif name == '__qualname__': + object.__delattr__(self, name) + delattr(self.__wrapped__, name) + + elif hasattr(type(self), name): + object.__delattr__(self, name) + + else: + delattr(self.__wrapped__, name) + + def __add__(self, other): + return self.__wrapped__ + other + + def __sub__(self, other): + return self.__wrapped__ - other + + def __mul__(self, other): + return self.__wrapped__ * other + + def __div__(self, other): + return operator.div(self.__wrapped__, other) + + def __truediv__(self, other): + return operator.truediv(self.__wrapped__, other) + + def __floordiv__(self, other): + return self.__wrapped__ // other + + def __mod__(self, other): + return self.__wrapped__ % other + + def __divmod__(self, other): + return divmod(self.__wrapped__, other) + + def __pow__(self, other, *args): + return pow(self.__wrapped__, other, *args) + + def __lshift__(self, other): + return self.__wrapped__ << other + + def __rshift__(self, other): + return self.__wrapped__ >> other + + def __and__(self, other): + return self.__wrapped__ & other + + def __xor__(self, other): + return self.__wrapped__ ^ other + + def __or__(self, other): + return self.__wrapped__ | other + + def __radd__(self, other): + return other + self.__wrapped__ + + def __rsub__(self, other): + return other - self.__wrapped__ + + def __rmul__(self, other): + return other * self.__wrapped__ + + def __rdiv__(self, other): + return operator.div(other, self.__wrapped__) + + def __rtruediv__(self, other): + return operator.truediv(other, self.__wrapped__) + + def __rfloordiv__(self, other): + return other // self.__wrapped__ + + def __rmod__(self, other): + return other % self.__wrapped__ + + def __rdivmod__(self, other): + return divmod(other, self.__wrapped__) + + def __rpow__(self, other, *args): + return pow(other, self.__wrapped__, *args) + + def __rlshift__(self, other): + return other << self.__wrapped__ + + def __rrshift__(self, other): + return other >> self.__wrapped__ + + def __rand__(self, other): + return other & self.__wrapped__ + + def __rxor__(self, other): + return other ^ self.__wrapped__ + + def __ror__(self, other): + return other | self.__wrapped__ + + def __iadd__(self, other): + self.__wrapped__ += other + return self + + def __isub__(self, other): + self.__wrapped__ -= other + return self + + def __imul__(self, other): + self.__wrapped__ *= other + return self + + def __idiv__(self, other): + self.__wrapped__ = operator.idiv(self.__wrapped__, other) + return self + + def __itruediv__(self, other): + self.__wrapped__ = operator.itruediv(self.__wrapped__, other) + return self + + def __ifloordiv__(self, other): + self.__wrapped__ //= other + return self + + def __imod__(self, other): + self.__wrapped__ %= other + return self + + def __ipow__(self, other): + self.__wrapped__ **= other + return self + + def __ilshift__(self, other): + self.__wrapped__ <<= other + return self + + def __irshift__(self, other): + self.__wrapped__ >>= other + return self + + def __iand__(self, other): + self.__wrapped__ &= other + return self + + def __ixor__(self, other): + self.__wrapped__ ^= other + return self + + def __ior__(self, other): + self.__wrapped__ |= other + return self + + def __neg__(self): + return -self.__wrapped__ + + def __pos__(self): + return +self.__wrapped__ + + def __abs__(self): + return abs(self.__wrapped__) + + def __invert__(self): + return ~self.__wrapped__ + + def __int__(self): + return int(self.__wrapped__) + + def __long__(self): + return long(self.__wrapped__) + + def __float__(self): + return float(self.__wrapped__) + + def __complex__(self): + return complex(self.__wrapped__) + + def __oct__(self): + return oct(self.__wrapped__) + + def __hex__(self): + return hex(self.__wrapped__) + + def __index__(self): + return operator.index(self.__wrapped__) + + def __len__(self): + return len(self.__wrapped__) + + def __contains__(self, value): + return value in self.__wrapped__ + + def __getitem__(self, key): + return self.__wrapped__[key] + + def __setitem__(self, key, value): + self.__wrapped__[key] = value + + def __delitem__(self, key): + del self.__wrapped__[key] + + def __getslice__(self, i, j): + return self.__wrapped__[i:j] + + def __setslice__(self, i, j, value): + self.__wrapped__[i:j] = value + + def __delslice__(self, i, j): + del self.__wrapped__[i:j] + + def __enter__(self): + return self.__wrapped__.__enter__() + + def __exit__(self, *args, **kwargs): + return self.__wrapped__.__exit__(*args, **kwargs) + + def __iter__(self): + return iter(self.__wrapped__) + + def __copy__(self): + raise NotImplementedError('object proxy must define __copy__()') + + def __deepcopy__(self, memo): + raise NotImplementedError('object proxy must define __deepcopy__()') + + def __reduce__(self): + raise NotImplementedError( + 'object proxy must define __reduce_ex__()') + + def __reduce_ex__(self, protocol): + raise NotImplementedError( + 'object proxy must define __reduce_ex__()') + +class CallableObjectProxy(ObjectProxy): + + def __call__(self, *args, **kwargs): + return self.__wrapped__(*args, **kwargs) + +class PartialCallableObjectProxy(ObjectProxy): + + def __init__(self, *args, **kwargs): + if len(args) < 1: + raise TypeError('partial type takes at least one argument') + + wrapped, args = args[0], args[1:] + + if not callable(wrapped): + raise TypeError('the first argument must be callable') + + super(PartialCallableObjectProxy, self).__init__(wrapped) + + self._self_args = args + self._self_kwargs = kwargs + + def __call__(self, *args, **kwargs): + _args = self._self_args + args + + _kwargs = dict(self._self_kwargs) + _kwargs.update(kwargs) + + return self.__wrapped__(*_args, **_kwargs) + +class _FunctionWrapperBase(ObjectProxy): + + __slots__ = ('_self_instance', '_self_wrapper', '_self_enabled', + '_self_binding', '_self_parent') + + def __init__(self, wrapped, instance, wrapper, enabled=None, + binding='function', parent=None): + + super(_FunctionWrapperBase, self).__init__(wrapped) + + object.__setattr__(self, '_self_instance', instance) + object.__setattr__(self, '_self_wrapper', wrapper) + object.__setattr__(self, '_self_enabled', enabled) + object.__setattr__(self, '_self_binding', binding) + object.__setattr__(self, '_self_parent', parent) + + def __get__(self, instance, owner): + # This method is actually doing double duty for both unbound and + # bound derived wrapper classes. It should possibly be broken up + # and the distinct functionality moved into the derived classes. + # Can't do that straight away due to some legacy code which is + # relying on it being here in this base class. + # + # The distinguishing attribute which determines whether we are + # being called in an unbound or bound wrapper is the parent + # attribute. If binding has never occurred, then the parent will + # be None. + # + # First therefore, is if we are called in an unbound wrapper. In + # this case we perform the binding. + # + # We have one special case to worry about here. This is where we + # are decorating a nested class. In this case the wrapped class + # would not have a __get__() method to call. In that case we + # simply return self. + # + # Note that we otherwise still do binding even if instance is + # None and accessing an unbound instance method from a class. + # This is because we need to be able to later detect that + # specific case as we will need to extract the instance from the + # first argument of those passed in. + + if self._self_parent is None: + if not inspect.isclass(self.__wrapped__): + descriptor = self.__wrapped__.__get__(instance, owner) + + return self.__bound_function_wrapper__(descriptor, instance, + self._self_wrapper, self._self_enabled, + self._self_binding, self) + + return self + + # Now we have the case of binding occurring a second time on what + # was already a bound function. In this case we would usually + # return ourselves again. This mirrors what Python does. + # + # The special case this time is where we were originally bound + # with an instance of None and we were likely an instance + # method. In that case we rebind against the original wrapped + # function from the parent again. + + if self._self_instance is None and self._self_binding == 'function': + descriptor = self._self_parent.__wrapped__.__get__( + instance, owner) + + return self._self_parent.__bound_function_wrapper__( + descriptor, instance, self._self_wrapper, + self._self_enabled, self._self_binding, + self._self_parent) + + return self + + def __call__(self, *args, **kwargs): + # If enabled has been specified, then evaluate it at this point + # and if the wrapper is not to be executed, then simply return + # the bound function rather than a bound wrapper for the bound + # function. When evaluating enabled, if it is callable we call + # it, otherwise we evaluate it as a boolean. + + if self._self_enabled is not None: + if callable(self._self_enabled): + if not self._self_enabled(): + return self.__wrapped__(*args, **kwargs) + elif not self._self_enabled: + return self.__wrapped__(*args, **kwargs) + + # This can occur where initial function wrapper was applied to + # a function that was already bound to an instance. In that case + # we want to extract the instance from the function and use it. + + if self._self_binding == 'function': + if self._self_instance is None: + instance = getattr(self.__wrapped__, '__self__', None) + if instance is not None: + return self._self_wrapper(self.__wrapped__, instance, + args, kwargs) + + # This is generally invoked when the wrapped function is being + # called as a normal function and is not bound to a class as an + # instance method. This is also invoked in the case where the + # wrapped function was a method, but this wrapper was in turn + # wrapped using the staticmethod decorator. + + return self._self_wrapper(self.__wrapped__, self._self_instance, + args, kwargs) + +class BoundFunctionWrapper(_FunctionWrapperBase): + + def __call__(self, *args, **kwargs): + # If enabled has been specified, then evaluate it at this point + # and if the wrapper is not to be executed, then simply return + # the bound function rather than a bound wrapper for the bound + # function. When evaluating enabled, if it is callable we call + # it, otherwise we evaluate it as a boolean. + + if self._self_enabled is not None: + if callable(self._self_enabled): + if not self._self_enabled(): + return self.__wrapped__(*args, **kwargs) + elif not self._self_enabled: + return self.__wrapped__(*args, **kwargs) + + # We need to do things different depending on whether we are + # likely wrapping an instance method vs a static method or class + # method. + + if self._self_binding == 'function': + if self._self_instance is None: + # This situation can occur where someone is calling the + # instancemethod via the class type and passing the instance + # as the first argument. We need to shift the args before + # making the call to the wrapper and effectively bind the + # instance to the wrapped function using a partial so the + # wrapper doesn't see anything as being different. + + if not args: + raise TypeError('missing 1 required positional argument') + + instance, args = args[0], args[1:] + wrapped = PartialCallableObjectProxy(self.__wrapped__, instance) + return self._self_wrapper(wrapped, instance, args, kwargs) + + return self._self_wrapper(self.__wrapped__, self._self_instance, + args, kwargs) + + else: + # As in this case we would be dealing with a classmethod or + # staticmethod, then _self_instance will only tell us whether + # when calling the classmethod or staticmethod they did it via an + # instance of the class it is bound to and not the case where + # done by the class type itself. We thus ignore _self_instance + # and use the __self__ attribute of the bound function instead. + # For a classmethod, this means instance will be the class type + # and for a staticmethod it will be None. This is probably the + # more useful thing we can pass through even though we loose + # knowledge of whether they were called on the instance vs the + # class type, as it reflects what they have available in the + # decoratored function. + + instance = getattr(self.__wrapped__, '__self__', None) + + return self._self_wrapper(self.__wrapped__, instance, args, + kwargs) + +class FunctionWrapper(_FunctionWrapperBase): + + __bound_function_wrapper__ = BoundFunctionWrapper + + def __init__(self, wrapped, wrapper, enabled=None): + # What it is we are wrapping here could be anything. We need to + # try and detect specific cases though. In particular, we need + # to detect when we are given something that is a method of a + # class. Further, we need to know when it is likely an instance + # method, as opposed to a class or static method. This can + # become problematic though as there isn't strictly a fool proof + # method of knowing. + # + # The situations we could encounter when wrapping a method are: + # + # 1. The wrapper is being applied as part of a decorator which + # is a part of the class definition. In this case what we are + # given is the raw unbound function, classmethod or staticmethod + # wrapper objects. + # + # The problem here is that we will not know we are being applied + # in the context of the class being set up. This becomes + # important later for the case of an instance method, because in + # that case we just see it as a raw function and can't + # distinguish it from wrapping a normal function outside of + # a class context. + # + # 2. The wrapper is being applied when performing monkey + # patching of the class type afterwards and the method to be + # wrapped was retrieved direct from the __dict__ of the class + # type. This is effectively the same as (1) above. + # + # 3. The wrapper is being applied when performing monkey + # patching of the class type afterwards and the method to be + # wrapped was retrieved from the class type. In this case + # binding will have been performed where the instance against + # which the method is bound will be None at that point. + # + # This case is a problem because we can no longer tell if the + # method was a static method, plus if using Python3, we cannot + # tell if it was an instance method as the concept of an + # unnbound method no longer exists. + # + # 4. The wrapper is being applied when performing monkey + # patching of an instance of a class. In this case binding will + # have been perfomed where the instance was not None. + # + # This case is a problem because we can no longer tell if the + # method was a static method. + # + # Overall, the best we can do is look at the original type of the + # object which was wrapped prior to any binding being done and + # see if it is an instance of classmethod or staticmethod. In + # the case where other decorators are between us and them, if + # they do not propagate the __class__ attribute so that the + # isinstance() checks works, then likely this will do the wrong + # thing where classmethod and staticmethod are used. + # + # Since it is likely to be very rare that anyone even puts + # decorators around classmethod and staticmethod, likelihood of + # that being an issue is very small, so we accept it and suggest + # that those other decorators be fixed. It is also only an issue + # if a decorator wants to actually do things with the arguments. + # + # As to not being able to identify static methods properly, we + # just hope that that isn't something people are going to want + # to wrap, or if they do suggest they do it the correct way by + # ensuring that it is decorated in the class definition itself, + # or patch it in the __dict__ of the class type. + # + # So to get the best outcome we can, whenever we aren't sure what + # it is, we label it as a 'function'. If it was already bound and + # that is rebound later, we assume that it will be an instance + # method and try an cope with the possibility that the 'self' + # argument it being passed as an explicit argument and shuffle + # the arguments around to extract 'self' for use as the instance. + + if isinstance(wrapped, classmethod): + binding = 'classmethod' + + elif isinstance(wrapped, staticmethod): + binding = 'staticmethod' + + elif hasattr(wrapped, '__self__'): + if inspect.isclass(wrapped.__self__): + binding = 'classmethod' + else: + binding = 'function' + + else: + binding = 'function' + + super(FunctionWrapper, self).__init__(wrapped, None, wrapper, + enabled, binding) + +try: + if not os.environ.get('WRAPT_DISABLE_EXTENSIONS'): + from ._wrappers import (ObjectProxy, CallableObjectProxy, + PartialCallableObjectProxy, FunctionWrapper, + BoundFunctionWrapper, _FunctionWrapperBase) +except ImportError: + pass + +# Helper functions for applying wrappers to existing functions. + +def resolve_path(module, name): + if isinstance(module, string_types): + __import__(module) + module = sys.modules[module] + + parent = module + + path = name.split('.') + attribute = path[0] + + # We can't just always use getattr() because in doing + # that on a class it will cause binding to occur which + # will complicate things later and cause some things not + # to work. For the case of a class we therefore access + # the __dict__ directly. To cope though with the wrong + # class being given to us, or a method being moved into + # a base class, we need to walk the class hierarchy to + # work out exactly which __dict__ the method was defined + # in, as accessing it from __dict__ will fail if it was + # not actually on the class given. Fallback to using + # getattr() if we can't find it. If it truly doesn't + # exist, then that will fail. + + def lookup_attribute(parent, attribute): + if inspect.isclass(parent): + for cls in inspect.getmro(parent): + if attribute in vars(cls): + return vars(cls)[attribute] + else: + return getattr(parent, attribute) + else: + return getattr(parent, attribute) + + original = lookup_attribute(parent, attribute) + + for attribute in path[1:]: + parent = original + original = lookup_attribute(parent, attribute) + + return (parent, attribute, original) + +def apply_patch(parent, attribute, replacement): + setattr(parent, attribute, replacement) + +def wrap_object(module, name, factory, args=(), kwargs={}): + (parent, attribute, original) = resolve_path(module, name) + wrapper = factory(original, *args, **kwargs) + apply_patch(parent, attribute, wrapper) + return wrapper + +# Function for applying a proxy object to an attribute of a class +# instance. The wrapper works by defining an attribute of the same name +# on the class which is a descriptor and which intercepts access to the +# instance attribute. Note that this cannot be used on attributes which +# are themselves defined by a property object. + +class AttributeWrapper(object): + + def __init__(self, attribute, factory, args, kwargs): + self.attribute = attribute + self.factory = factory + self.args = args + self.kwargs = kwargs + + def __get__(self, instance, owner): + value = instance.__dict__[self.attribute] + return self.factory(value, *self.args, **self.kwargs) + + def __set__(self, instance, value): + instance.__dict__[self.attribute] = value + + def __delete__(self, instance): + del instance.__dict__[self.attribute] + +def wrap_object_attribute(module, name, factory, args=(), kwargs={}): + path, attribute = name.rsplit('.', 1) + parent = resolve_path(module, path)[2] + wrapper = AttributeWrapper(attribute, factory, args, kwargs) + apply_patch(parent, attribute, wrapper) + return wrapper + +# Functions for creating a simple decorator using a FunctionWrapper, +# plus short cut functions for applying wrappers to functions. These are +# for use when doing monkey patching. For a more featured way of +# creating decorators see the decorator decorator instead. + +def function_wrapper(wrapper): + def _wrapper(wrapped, instance, args, kwargs): + target_wrapped = args[0] + if instance is None: + target_wrapper = wrapper + elif inspect.isclass(instance): + target_wrapper = wrapper.__get__(None, instance) + else: + target_wrapper = wrapper.__get__(instance, type(instance)) + return FunctionWrapper(target_wrapped, target_wrapper) + return FunctionWrapper(wrapper, _wrapper) + +def wrap_function_wrapper(module, name, wrapper): + return wrap_object(module, name, FunctionWrapper, (wrapper,)) + +def patch_function_wrapper(module, name): + def _wrapper(wrapper): + return wrap_object(module, name, FunctionWrapper, (wrapper,)) + return _wrapper + +def transient_function_wrapper(module, name): + def _decorator(wrapper): + def _wrapper(wrapped, instance, args, kwargs): + target_wrapped = args[0] + if instance is None: + target_wrapper = wrapper + elif inspect.isclass(instance): + target_wrapper = wrapper.__get__(None, instance) + else: + target_wrapper = wrapper.__get__(instance, type(instance)) + def _execute(wrapped, instance, args, kwargs): + (parent, attribute, original) = resolve_path(module, name) + replacement = FunctionWrapper(original, target_wrapper) + setattr(parent, attribute, replacement) + try: + return wrapped(*args, **kwargs) + finally: + setattr(parent, attribute, original) + return FunctionWrapper(target_wrapped, _execute) + return FunctionWrapper(wrapper, _wrapper) + return _decorator + +# A weak function proxy. This will work on instance methods, class +# methods, static methods and regular functions. Special treatment is +# needed for the method types because the bound method is effectively a +# transient object and applying a weak reference to one will immediately +# result in it being destroyed and the weakref callback called. The weak +# reference is therefore applied to the instance the method is bound to +# and the original function. The function is then rebound at the point +# of a call via the weak function proxy. + +def _weak_function_proxy_callback(ref, proxy, callback): + if proxy._self_expired: + return + + proxy._self_expired = True + + # This could raise an exception. We let it propagate back and let + # the weakref.proxy() deal with it, at which point it generally + # prints out a short error message direct to stderr and keeps going. + + if callback is not None: + callback(proxy) + +class WeakFunctionProxy(ObjectProxy): + + __slots__ = ('_self_expired', '_self_instance') + + def __init__(self, wrapped, callback=None): + # We need to determine if the wrapped function is actually a + # bound method. In the case of a bound method, we need to keep a + # reference to the original unbound function and the instance. + # This is necessary because if we hold a reference to the bound + # function, it will be the only reference and given it is a + # temporary object, it will almost immediately expire and + # the weakref callback triggered. So what is done is that we + # hold a reference to the instance and unbound function and + # when called bind the function to the instance once again and + # then call it. Note that we avoid using a nested function for + # the callback here so as not to cause any odd reference cycles. + + _callback = callback and functools.partial( + _weak_function_proxy_callback, proxy=self, + callback=callback) + + self._self_expired = False + + if isinstance(wrapped, _FunctionWrapperBase): + self._self_instance = weakref.ref(wrapped._self_instance, + _callback) + + if wrapped._self_parent is not None: + super(WeakFunctionProxy, self).__init__( + weakref.proxy(wrapped._self_parent, _callback)) + + else: + super(WeakFunctionProxy, self).__init__( + weakref.proxy(wrapped, _callback)) + + return + + try: + self._self_instance = weakref.ref(wrapped.__self__, _callback) + + super(WeakFunctionProxy, self).__init__( + weakref.proxy(wrapped.__func__, _callback)) + + except AttributeError: + self._self_instance = None + + super(WeakFunctionProxy, self).__init__( + weakref.proxy(wrapped, _callback)) + + def __call__(self, *args, **kwargs): + # We perform a boolean check here on the instance and wrapped + # function as that will trigger the reference error prior to + # calling if the reference had expired. + + instance = self._self_instance and self._self_instance() + function = self.__wrapped__ and self.__wrapped__ + + # If the wrapped function was originally a bound function, for + # which we retained a reference to the instance and the unbound + # function we need to rebind the function and then call it. If + # not just called the wrapped function. + + if instance is None: + return self.__wrapped__(*args, **kwargs) + + return function.__get__(instance, type(instance))(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace/version.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace/version.py new file mode 100644 index 000000000..270ae17a1 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace/version.py @@ -0,0 +1,16 @@ +def get_version(): + # type: () -> str + try: + from ._version import version + + return version + except ImportError: + import pkg_resources + + try: + # something went wrong while creating _version.py, let's fallback to pkg_resources + + return pkg_resources.get_distribution(__name__).version + except pkg_resources.DistributionNotFound: + # package is not installed + return "dev" diff --git a/flask-app/venv/lib/python3.8/site-packages/ddtrace_gevent_check.py b/flask-app/venv/lib/python3.8/site-packages/ddtrace_gevent_check.py new file mode 100644 index 000000000..cfbe5ce77 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/ddtrace_gevent_check.py @@ -0,0 +1,13 @@ +import sys +import warnings + + +def gevent_patch_all(event): + if "ddtrace" in sys.modules: + warnings.warn( + "Loading ddtrace before using gevent monkey patching is not supported " + "and is likely to break the application. " + "Use `DD_GEVENT_PATCH_ALL=true ddtrace-run` to fix this or " + "import `ddtrace` after `gevent.monkey.patch_all()` has been called.", + RuntimeWarning, + ) diff --git a/flask-app/venv/lib/python3.8/site-packages/easy_install.py b/flask-app/venv/lib/python3.8/site-packages/easy_install.py new file mode 100644 index 000000000..d87e98403 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/easy_install.py @@ -0,0 +1,5 @@ +"""Run the EasyInstall command""" + +if __name__ == '__main__': + from setuptools.command.easy_install import main + main() diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__init__.py b/flask-app/venv/lib/python3.8/site-packages/flask/__init__.py new file mode 100644 index 000000000..43b54683e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/flask/__init__.py @@ -0,0 +1,46 @@ +from markupsafe import escape +from markupsafe import Markup +from werkzeug.exceptions import abort as abort +from werkzeug.utils import redirect as redirect + +from . import json as json +from .app import Flask as Flask +from .app import Request as Request +from .app import Response as Response +from .blueprints import Blueprint as Blueprint +from .config import Config as Config +from .ctx import after_this_request as after_this_request +from .ctx import copy_current_request_context as copy_current_request_context +from .ctx import has_app_context as has_app_context +from .ctx import has_request_context as has_request_context +from .globals import _app_ctx_stack as _app_ctx_stack +from .globals import _request_ctx_stack as _request_ctx_stack +from .globals import current_app as current_app +from .globals import g as g +from .globals import request as request +from .globals import session as session +from .helpers import flash as flash +from .helpers import get_flashed_messages as get_flashed_messages +from .helpers import get_template_attribute as get_template_attribute +from .helpers import make_response as make_response +from .helpers import safe_join as safe_join +from .helpers import send_file as send_file +from .helpers import send_from_directory as send_from_directory +from .helpers import stream_with_context as stream_with_context +from .helpers import url_for as url_for +from .json import jsonify as jsonify +from .signals import appcontext_popped as appcontext_popped +from .signals import appcontext_pushed as appcontext_pushed +from .signals import appcontext_tearing_down as appcontext_tearing_down +from .signals import before_render_template as before_render_template +from .signals import got_request_exception as got_request_exception +from .signals import message_flashed as message_flashed +from .signals import request_finished as request_finished +from .signals import request_started as request_started +from .signals import request_tearing_down as request_tearing_down +from .signals import signals_available as signals_available +from .signals import template_rendered as template_rendered +from .templating import render_template as render_template +from .templating import render_template_string as render_template_string + +__version__ = "2.0.2" diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__main__.py b/flask-app/venv/lib/python3.8/site-packages/flask/__main__.py new file mode 100644 index 000000000..4e28416e1 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/flask/__main__.py @@ -0,0 +1,3 @@ +from .cli import main + +main() diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..bc144d3c5 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/__main__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/__main__.cpython-38.pyc new file mode 100644 index 000000000..518278fbf Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/__main__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/app.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/app.cpython-38.pyc new file mode 100644 index 000000000..f6fd2820f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/app.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/blueprints.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/blueprints.cpython-38.pyc new file mode 100644 index 000000000..cac5ac375 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/blueprints.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/cli.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/cli.cpython-38.pyc new file mode 100644 index 000000000..4778c59c8 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/cli.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/config.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/config.cpython-38.pyc new file mode 100644 index 000000000..e7b33fb29 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/config.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/ctx.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/ctx.cpython-38.pyc new file mode 100644 index 000000000..1d2543790 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/ctx.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/debughelpers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/debughelpers.cpython-38.pyc new file mode 100644 index 000000000..9c41ded50 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/debughelpers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/globals.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/globals.cpython-38.pyc new file mode 100644 index 000000000..cdfa1f153 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/globals.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/helpers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/helpers.cpython-38.pyc new file mode 100644 index 000000000..b22bdafa2 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/helpers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/logging.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/logging.cpython-38.pyc new file mode 100644 index 000000000..461c3753c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/logging.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/scaffold.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/scaffold.cpython-38.pyc new file mode 100644 index 000000000..c0e48c090 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/scaffold.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/sessions.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/sessions.cpython-38.pyc new file mode 100644 index 000000000..9927a0017 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/sessions.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/signals.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/signals.cpython-38.pyc new file mode 100644 index 000000000..eac56f1f4 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/signals.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/templating.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/templating.cpython-38.pyc new file mode 100644 index 000000000..7ec84e6ef Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/templating.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/testing.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/testing.cpython-38.pyc new file mode 100644 index 000000000..1d9ee89ed Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/testing.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/typing.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/typing.cpython-38.pyc new file mode 100644 index 000000000..c6b548412 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/typing.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/views.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/views.cpython-38.pyc new file mode 100644 index 000000000..7ab1a6d17 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/views.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/wrappers.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/wrappers.cpython-38.pyc new file mode 100644 index 000000000..853f41842 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/flask/__pycache__/wrappers.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/app.py b/flask-app/venv/lib/python3.8/site-packages/flask/app.py new file mode 100644 index 000000000..23b99e2ca --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/flask/app.py @@ -0,0 +1,2091 @@ +import functools +import inspect +import logging +import os +import sys +import typing as t +import weakref +from datetime import timedelta +from itertools import chain +from threading import Lock +from types import TracebackType + +from werkzeug.datastructures import Headers +from werkzeug.datastructures import ImmutableDict +from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import BadRequestKeyError +from werkzeug.exceptions import HTTPException +from werkzeug.exceptions import InternalServerError +from werkzeug.local import ContextVar +from werkzeug.routing import BuildError +from werkzeug.routing import Map +from werkzeug.routing import MapAdapter +from werkzeug.routing import RequestRedirect +from werkzeug.routing import RoutingException +from werkzeug.routing import Rule +from werkzeug.wrappers import Response as BaseResponse + +from . import cli +from . import json +from .config import Config +from .config import ConfigAttribute +from .ctx import _AppCtxGlobals +from .ctx import AppContext +from .ctx import RequestContext +from .globals import _request_ctx_stack +from .globals import g +from .globals import request +from .globals import session +from .helpers import _split_blueprint_path +from .helpers import get_debug_flag +from .helpers import get_env +from .helpers import get_flashed_messages +from .helpers import get_load_dotenv +from .helpers import locked_cached_property +from .helpers import url_for +from .json import jsonify +from .logging import create_logger +from .scaffold import _endpoint_from_view_func +from .scaffold import _sentinel +from .scaffold import find_package +from .scaffold import Scaffold +from .scaffold import setupmethod +from .sessions import SecureCookieSessionInterface +from .signals import appcontext_tearing_down +from .signals import got_request_exception +from .signals import request_finished +from .signals import request_started +from .signals import request_tearing_down +from .templating import DispatchingJinjaLoader +from .templating import Environment +from .typing import BeforeFirstRequestCallable +from .typing import ResponseReturnValue +from .typing import TeardownCallable +from .typing import TemplateFilterCallable +from .typing import TemplateGlobalCallable +from .typing import TemplateTestCallable +from .wrappers import Request +from .wrappers import Response + +if t.TYPE_CHECKING: + import typing_extensions as te + from .blueprints import Blueprint + from .testing import FlaskClient + from .testing import FlaskCliRunner + from .typing import ErrorHandlerCallable + +if sys.version_info >= (3, 8): + iscoroutinefunction = inspect.iscoroutinefunction +else: + + def iscoroutinefunction(func: t.Any) -> bool: + while inspect.ismethod(func): + func = func.__func__ + + while isinstance(func, functools.partial): + func = func.func + + return inspect.iscoroutinefunction(func) + + +def _make_timedelta(value: t.Optional[timedelta]) -> t.Optional[timedelta]: + if value is None or isinstance(value, timedelta): + return value + + return timedelta(seconds=value) + + +class Flask(Scaffold): + """The flask object implements a WSGI application and acts as the central + object. It is passed the name of the module or package of the + application. Once it is created it will act as a central registry for + the view functions, the URL rules, template configuration and much more. + + The name of the package is used to resolve resources from inside the + package or the folder the module is contained in depending on if the + package parameter resolves to an actual python package (a folder with + an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file). + + For more information about resource loading, see :func:`open_resource`. + + Usually you create a :class:`Flask` instance in your main module or + in the :file:`__init__.py` file of your package like this:: + + from flask import Flask + app = Flask(__name__) + + .. admonition:: About the First Parameter + + The idea of the first parameter is to give Flask an idea of what + belongs to your application. This name is used to find resources + on the filesystem, can be used by extensions to improve debugging + information and a lot more. + + So it's important what you provide there. If you are using a single + module, `__name__` is always the correct value. If you however are + using a package, it's usually recommended to hardcode the name of + your package there. + + For example if your application is defined in :file:`yourapplication/app.py` + you should create it with one of the two versions below:: + + app = Flask('yourapplication') + app = Flask(__name__.split('.')[0]) + + Why is that? The application will work even with `__name__`, thanks + to how resources are looked up. However it will make debugging more + painful. Certain extensions can make assumptions based on the + import name of your application. For example the Flask-SQLAlchemy + extension will look for the code in your application that triggered + an SQL query in debug mode. If the import name is not properly set + up, that debugging information is lost. (For example it would only + pick up SQL queries in `yourapplication.app` and not + `yourapplication.views.frontend`) + + .. versionadded:: 0.7 + The `static_url_path`, `static_folder`, and `template_folder` + parameters were added. + + .. versionadded:: 0.8 + The `instance_path` and `instance_relative_config` parameters were + added. + + .. versionadded:: 0.11 + The `root_path` parameter was added. + + .. versionadded:: 1.0 + The ``host_matching`` and ``static_host`` parameters were added. + + .. versionadded:: 1.0 + The ``subdomain_matching`` parameter was added. Subdomain + matching needs to be enabled manually now. Setting + :data:`SERVER_NAME` does not implicitly enable it. + + :param import_name: the name of the application package + :param static_url_path: can be used to specify a different path for the + static files on the web. Defaults to the name + of the `static_folder` folder. + :param static_folder: The folder with static files that is served at + ``static_url_path``. Relative to the application ``root_path`` + or an absolute path. Defaults to ``'static'``. + :param static_host: the host to use when adding the static route. + Defaults to None. Required when using ``host_matching=True`` + with a ``static_folder`` configured. + :param host_matching: set ``url_map.host_matching`` attribute. + Defaults to False. + :param subdomain_matching: consider the subdomain relative to + :data:`SERVER_NAME` when matching routes. Defaults to False. + :param template_folder: the folder that contains the templates that should + be used by the application. Defaults to + ``'templates'`` folder in the root path of the + application. + :param instance_path: An alternative instance path for the application. + By default the folder ``'instance'`` next to the + package or module is assumed to be the instance + path. + :param instance_relative_config: if set to ``True`` relative filenames + for loading the config are assumed to + be relative to the instance path instead + of the application root. + :param root_path: The path to the root of the application files. + This should only be set manually when it can't be detected + automatically, such as for namespace packages. + """ + + #: The class that is used for request objects. See :class:`~flask.Request` + #: for more information. + request_class = Request + + #: The class that is used for response objects. See + #: :class:`~flask.Response` for more information. + response_class = Response + + #: The class that is used for the Jinja environment. + #: + #: .. versionadded:: 0.11 + jinja_environment = Environment + + #: The class that is used for the :data:`~flask.g` instance. + #: + #: Example use cases for a custom class: + #: + #: 1. Store arbitrary attributes on flask.g. + #: 2. Add a property for lazy per-request database connectors. + #: 3. Return None instead of AttributeError on unexpected attributes. + #: 4. Raise exception if an unexpected attr is set, a "controlled" flask.g. + #: + #: In Flask 0.9 this property was called `request_globals_class` but it + #: was changed in 0.10 to :attr:`app_ctx_globals_class` because the + #: flask.g object is now application context scoped. + #: + #: .. versionadded:: 0.10 + app_ctx_globals_class = _AppCtxGlobals + + #: The class that is used for the ``config`` attribute of this app. + #: Defaults to :class:`~flask.Config`. + #: + #: Example use cases for a custom class: + #: + #: 1. Default values for certain config options. + #: 2. Access to config values through attributes in addition to keys. + #: + #: .. versionadded:: 0.11 + config_class = Config + + #: The testing flag. Set this to ``True`` to enable the test mode of + #: Flask extensions (and in the future probably also Flask itself). + #: For example this might activate test helpers that have an + #: additional runtime cost which should not be enabled by default. + #: + #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the + #: default it's implicitly enabled. + #: + #: This attribute can also be configured from the config with the + #: ``TESTING`` configuration key. Defaults to ``False``. + testing = ConfigAttribute("TESTING") + + #: If a secret key is set, cryptographic components can use this to + #: sign cookies and other things. Set this to a complex random value + #: when you want to use the secure cookie for instance. + #: + #: This attribute can also be configured from the config with the + #: :data:`SECRET_KEY` configuration key. Defaults to ``None``. + secret_key = ConfigAttribute("SECRET_KEY") + + #: The secure cookie uses this for the name of the session cookie. + #: + #: This attribute can also be configured from the config with the + #: ``SESSION_COOKIE_NAME`` configuration key. Defaults to ``'session'`` + session_cookie_name = ConfigAttribute("SESSION_COOKIE_NAME") + + #: A :class:`~datetime.timedelta` which is used to set the expiration + #: date of a permanent session. The default is 31 days which makes a + #: permanent session survive for roughly one month. + #: + #: This attribute can also be configured from the config with the + #: ``PERMANENT_SESSION_LIFETIME`` configuration key. Defaults to + #: ``timedelta(days=31)`` + permanent_session_lifetime = ConfigAttribute( + "PERMANENT_SESSION_LIFETIME", get_converter=_make_timedelta + ) + + #: A :class:`~datetime.timedelta` or number of seconds which is used + #: as the default ``max_age`` for :func:`send_file`. The default is + #: ``None``, which tells the browser to use conditional requests + #: instead of a timed cache. + #: + #: Configured with the :data:`SEND_FILE_MAX_AGE_DEFAULT` + #: configuration key. + #: + #: .. versionchanged:: 2.0 + #: Defaults to ``None`` instead of 12 hours. + send_file_max_age_default = ConfigAttribute( + "SEND_FILE_MAX_AGE_DEFAULT", get_converter=_make_timedelta + ) + + #: Enable this if you want to use the X-Sendfile feature. Keep in + #: mind that the server has to support this. This only affects files + #: sent with the :func:`send_file` method. + #: + #: .. versionadded:: 0.2 + #: + #: This attribute can also be configured from the config with the + #: ``USE_X_SENDFILE`` configuration key. Defaults to ``False``. + use_x_sendfile = ConfigAttribute("USE_X_SENDFILE") + + #: The JSON encoder class to use. Defaults to :class:`~flask.json.JSONEncoder`. + #: + #: .. versionadded:: 0.10 + json_encoder = json.JSONEncoder + + #: The JSON decoder class to use. Defaults to :class:`~flask.json.JSONDecoder`. + #: + #: .. versionadded:: 0.10 + json_decoder = json.JSONDecoder + + #: Options that are passed to the Jinja environment in + #: :meth:`create_jinja_environment`. Changing these options after + #: the environment is created (accessing :attr:`jinja_env`) will + #: have no effect. + #: + #: .. versionchanged:: 1.1.0 + #: This is a ``dict`` instead of an ``ImmutableDict`` to allow + #: easier configuration. + #: + jinja_options: dict = {} + + #: Default configuration parameters. + default_config = ImmutableDict( + { + "ENV": None, + "DEBUG": None, + "TESTING": False, + "PROPAGATE_EXCEPTIONS": None, + "PRESERVE_CONTEXT_ON_EXCEPTION": None, + "SECRET_KEY": None, + "PERMANENT_SESSION_LIFETIME": timedelta(days=31), + "USE_X_SENDFILE": False, + "SERVER_NAME": None, + "APPLICATION_ROOT": "/", + "SESSION_COOKIE_NAME": "session", + "SESSION_COOKIE_DOMAIN": None, + "SESSION_COOKIE_PATH": None, + "SESSION_COOKIE_HTTPONLY": True, + "SESSION_COOKIE_SECURE": False, + "SESSION_COOKIE_SAMESITE": None, + "SESSION_REFRESH_EACH_REQUEST": True, + "MAX_CONTENT_LENGTH": None, + "SEND_FILE_MAX_AGE_DEFAULT": None, + "TRAP_BAD_REQUEST_ERRORS": None, + "TRAP_HTTP_EXCEPTIONS": False, + "EXPLAIN_TEMPLATE_LOADING": False, + "PREFERRED_URL_SCHEME": "http", + "JSON_AS_ASCII": True, + "JSON_SORT_KEYS": True, + "JSONIFY_PRETTYPRINT_REGULAR": False, + "JSONIFY_MIMETYPE": "application/json", + "TEMPLATES_AUTO_RELOAD": None, + "MAX_COOKIE_SIZE": 4093, + } + ) + + #: The rule object to use for URL rules created. This is used by + #: :meth:`add_url_rule`. Defaults to :class:`werkzeug.routing.Rule`. + #: + #: .. versionadded:: 0.7 + url_rule_class = Rule + + #: The map object to use for storing the URL rules and routing + #: configuration parameters. Defaults to :class:`werkzeug.routing.Map`. + #: + #: .. versionadded:: 1.1.0 + url_map_class = Map + + #: The :meth:`test_client` method creates an instance of this test + #: client class. Defaults to :class:`~flask.testing.FlaskClient`. + #: + #: .. versionadded:: 0.7 + test_client_class: t.Optional[t.Type["FlaskClient"]] = None + + #: The :class:`~click.testing.CliRunner` subclass, by default + #: :class:`~flask.testing.FlaskCliRunner` that is used by + #: :meth:`test_cli_runner`. Its ``__init__`` method should take a + #: Flask app object as the first argument. + #: + #: .. versionadded:: 1.0 + test_cli_runner_class: t.Optional[t.Type["FlaskCliRunner"]] = None + + #: the session interface to use. By default an instance of + #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. + #: + #: .. versionadded:: 0.8 + session_interface = SecureCookieSessionInterface() + + def __init__( + self, + import_name: str, + static_url_path: t.Optional[str] = None, + static_folder: t.Optional[t.Union[str, os.PathLike]] = "static", + static_host: t.Optional[str] = None, + host_matching: bool = False, + subdomain_matching: bool = False, + template_folder: t.Optional[str] = "templates", + instance_path: t.Optional[str] = None, + instance_relative_config: bool = False, + root_path: t.Optional[str] = None, + ): + super().__init__( + import_name=import_name, + static_folder=static_folder, + static_url_path=static_url_path, + template_folder=template_folder, + root_path=root_path, + ) + + if instance_path is None: + instance_path = self.auto_find_instance_path() + elif not os.path.isabs(instance_path): + raise ValueError( + "If an instance path is provided it must be absolute." + " A relative path was given instead." + ) + + #: Holds the path to the instance folder. + #: + #: .. versionadded:: 0.8 + self.instance_path = instance_path + + #: The configuration dictionary as :class:`Config`. This behaves + #: exactly like a regular dictionary but supports additional methods + #: to load a config from files. + self.config = self.make_config(instance_relative_config) + + #: A list of functions that are called when :meth:`url_for` raises a + #: :exc:`~werkzeug.routing.BuildError`. Each function registered here + #: is called with `error`, `endpoint` and `values`. If a function + #: returns ``None`` or raises a :exc:`BuildError` the next function is + #: tried. + #: + #: .. versionadded:: 0.9 + self.url_build_error_handlers: t.List[ + t.Callable[[Exception, str, dict], str] + ] = [] + + #: A list of functions that will be called at the beginning of the + #: first request to this instance. To register a function, use the + #: :meth:`before_first_request` decorator. + #: + #: .. versionadded:: 0.8 + self.before_first_request_funcs: t.List[BeforeFirstRequestCallable] = [] + + #: A list of functions that are called when the application context + #: is destroyed. Since the application context is also torn down + #: if the request ends this is the place to store code that disconnects + #: from databases. + #: + #: .. versionadded:: 0.9 + self.teardown_appcontext_funcs: t.List[TeardownCallable] = [] + + #: A list of shell context processor functions that should be run + #: when a shell context is created. + #: + #: .. versionadded:: 0.11 + self.shell_context_processors: t.List[t.Callable[[], t.Dict[str, t.Any]]] = [] + + #: Maps registered blueprint names to blueprint objects. The + #: dict retains the order the blueprints were registered in. + #: Blueprints can be registered multiple times, this dict does + #: not track how often they were attached. + #: + #: .. versionadded:: 0.7 + self.blueprints: t.Dict[str, "Blueprint"] = {} + + #: a place where extensions can store application specific state. For + #: example this is where an extension could store database engines and + #: similar things. + #: + #: The key must match the name of the extension module. For example in + #: case of a "Flask-Foo" extension in `flask_foo`, the key would be + #: ``'foo'``. + #: + #: .. versionadded:: 0.7 + self.extensions: dict = {} + + #: The :class:`~werkzeug.routing.Map` for this instance. You can use + #: this to change the routing converters after the class was created + #: but before any routes are connected. Example:: + #: + #: from werkzeug.routing import BaseConverter + #: + #: class ListConverter(BaseConverter): + #: def to_python(self, value): + #: return value.split(',') + #: def to_url(self, values): + #: return ','.join(super(ListConverter, self).to_url(value) + #: for value in values) + #: + #: app = Flask(__name__) + #: app.url_map.converters['list'] = ListConverter + self.url_map = self.url_map_class() + + self.url_map.host_matching = host_matching + self.subdomain_matching = subdomain_matching + + # tracks internally if the application already handled at least one + # request. + self._got_first_request = False + self._before_request_lock = Lock() + + # Add a static route using the provided static_url_path, static_host, + # and static_folder if there is a configured static_folder. + # Note we do this without checking if static_folder exists. + # For one, it might be created while the server is running (e.g. during + # development). Also, Google App Engine stores static files somewhere + if self.has_static_folder: + assert ( + bool(static_host) == host_matching + ), "Invalid static_host/host_matching combination" + # Use a weakref to avoid creating a reference cycle between the app + # and the view function (see #3761). + self_ref = weakref.ref(self) + self.add_url_rule( + f"{self.static_url_path}/", + endpoint="static", + host=static_host, + view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 + ) + + # Set the name of the Click group in case someone wants to add + # the app's commands to another CLI tool. + self.cli.name = self.name + + def _is_setup_finished(self) -> bool: + return self.debug and self._got_first_request + + @locked_cached_property + def name(self) -> str: # type: ignore + """The name of the application. This is usually the import name + with the difference that it's guessed from the run file if the + import name is main. This name is used as a display name when + Flask needs the name of the application. It can be set and overridden + to change the value. + + .. versionadded:: 0.8 + """ + if self.import_name == "__main__": + fn = getattr(sys.modules["__main__"], "__file__", None) + if fn is None: + return "__main__" + return os.path.splitext(os.path.basename(fn))[0] + return self.import_name + + @property + def propagate_exceptions(self) -> bool: + """Returns the value of the ``PROPAGATE_EXCEPTIONS`` configuration + value in case it's set, otherwise a sensible default is returned. + + .. versionadded:: 0.7 + """ + rv = self.config["PROPAGATE_EXCEPTIONS"] + if rv is not None: + return rv + return self.testing or self.debug + + @property + def preserve_context_on_exception(self) -> bool: + """Returns the value of the ``PRESERVE_CONTEXT_ON_EXCEPTION`` + configuration value in case it's set, otherwise a sensible default + is returned. + + .. versionadded:: 0.7 + """ + rv = self.config["PRESERVE_CONTEXT_ON_EXCEPTION"] + if rv is not None: + return rv + return self.debug + + @locked_cached_property + def logger(self) -> logging.Logger: + """A standard Python :class:`~logging.Logger` for the app, with + the same name as :attr:`name`. + + In debug mode, the logger's :attr:`~logging.Logger.level` will + be set to :data:`~logging.DEBUG`. + + If there are no handlers configured, a default handler will be + added. See :doc:`/logging` for more information. + + .. versionchanged:: 1.1.0 + The logger takes the same name as :attr:`name` rather than + hard-coding ``"flask.app"``. + + .. versionchanged:: 1.0.0 + Behavior was simplified. The logger is always named + ``"flask.app"``. The level is only set during configuration, + it doesn't check ``app.debug`` each time. Only one format is + used, not different ones depending on ``app.debug``. No + handlers are removed, and a handler is only added if no + handlers are already configured. + + .. versionadded:: 0.3 + """ + return create_logger(self) + + @locked_cached_property + def jinja_env(self) -> Environment: + """The Jinja environment used to load templates. + + The environment is created the first time this property is + accessed. Changing :attr:`jinja_options` after that will have no + effect. + """ + return self.create_jinja_environment() + + @property + def got_first_request(self) -> bool: + """This attribute is set to ``True`` if the application started + handling the first request. + + .. versionadded:: 0.8 + """ + return self._got_first_request + + def make_config(self, instance_relative: bool = False) -> Config: + """Used to create the config attribute by the Flask constructor. + The `instance_relative` parameter is passed in from the constructor + of Flask (there named `instance_relative_config`) and indicates if + the config should be relative to the instance path or the root path + of the application. + + .. versionadded:: 0.8 + """ + root_path = self.root_path + if instance_relative: + root_path = self.instance_path + defaults = dict(self.default_config) + defaults["ENV"] = get_env() + defaults["DEBUG"] = get_debug_flag() + return self.config_class(root_path, defaults) + + def auto_find_instance_path(self) -> str: + """Tries to locate the instance path if it was not provided to the + constructor of the application class. It will basically calculate + the path to a folder named ``instance`` next to your main file or + the package. + + .. versionadded:: 0.8 + """ + prefix, package_path = find_package(self.import_name) + if prefix is None: + return os.path.join(package_path, "instance") + return os.path.join(prefix, "var", f"{self.name}-instance") + + def open_instance_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]: + """Opens a resource from the application's instance folder + (:attr:`instance_path`). Otherwise works like + :meth:`open_resource`. Instance resources can also be opened for + writing. + + :param resource: the name of the resource. To access resources within + subfolders use forward slashes as separator. + :param mode: resource file opening mode, default is 'rb'. + """ + return open(os.path.join(self.instance_path, resource), mode) + + @property + def templates_auto_reload(self) -> bool: + """Reload templates when they are changed. Used by + :meth:`create_jinja_environment`. + + This attribute can be configured with :data:`TEMPLATES_AUTO_RELOAD`. If + not set, it will be enabled in debug mode. + + .. versionadded:: 1.0 + This property was added but the underlying config and behavior + already existed. + """ + rv = self.config["TEMPLATES_AUTO_RELOAD"] + return rv if rv is not None else self.debug + + @templates_auto_reload.setter + def templates_auto_reload(self, value: bool) -> None: + self.config["TEMPLATES_AUTO_RELOAD"] = value + + def create_jinja_environment(self) -> Environment: + """Create the Jinja environment based on :attr:`jinja_options` + and the various Jinja-related methods of the app. Changing + :attr:`jinja_options` after this will have no effect. Also adds + Flask-related globals and filters to the environment. + + .. versionchanged:: 0.11 + ``Environment.auto_reload`` set in accordance with + ``TEMPLATES_AUTO_RELOAD`` configuration option. + + .. versionadded:: 0.5 + """ + options = dict(self.jinja_options) + + if "autoescape" not in options: + options["autoescape"] = self.select_jinja_autoescape + + if "auto_reload" not in options: + options["auto_reload"] = self.templates_auto_reload + + rv = self.jinja_environment(self, **options) + rv.globals.update( + url_for=url_for, + get_flashed_messages=get_flashed_messages, + config=self.config, + # request, session and g are normally added with the + # context processor for efficiency reasons but for imported + # templates we also want the proxies in there. + request=request, + session=session, + g=g, + ) + rv.policies["json.dumps_function"] = json.dumps + return rv + + def create_global_jinja_loader(self) -> DispatchingJinjaLoader: + """Creates the loader for the Jinja2 environment. Can be used to + override just the loader and keeping the rest unchanged. It's + discouraged to override this function. Instead one should override + the :meth:`jinja_loader` function instead. + + The global loader dispatches between the loaders of the application + and the individual blueprints. + + .. versionadded:: 0.7 + """ + return DispatchingJinjaLoader(self) + + def select_jinja_autoescape(self, filename: str) -> bool: + """Returns ``True`` if autoescaping should be active for the given + template name. If no template name is given, returns `True`. + + .. versionadded:: 0.5 + """ + if filename is None: + return True + return filename.endswith((".html", ".htm", ".xml", ".xhtml")) + + def update_template_context(self, context: dict) -> None: + """Update the template context with some commonly used variables. + This injects request, session, config and g into the template + context as well as everything template context processors want + to inject. Note that the as of Flask 0.6, the original values + in the context will not be overridden if a context processor + decides to return a value with the same key. + + :param context: the context as a dictionary that is updated in place + to add extra variables. + """ + names: t.Iterable[t.Optional[str]] = (None,) + + # A template may be rendered outside a request context. + if request: + names = chain(names, reversed(request.blueprints)) + + # The values passed to render_template take precedence. Keep a + # copy to re-apply after all context functions. + orig_ctx = context.copy() + + for name in names: + if name in self.template_context_processors: + for func in self.template_context_processors[name]: + context.update(func()) + + context.update(orig_ctx) + + def make_shell_context(self) -> dict: + """Returns the shell context for an interactive shell for this + application. This runs all the registered shell context + processors. + + .. versionadded:: 0.11 + """ + rv = {"app": self, "g": g} + for processor in self.shell_context_processors: + rv.update(processor()) + return rv + + #: What environment the app is running in. Flask and extensions may + #: enable behaviors based on the environment, such as enabling debug + #: mode. This maps to the :data:`ENV` config key. This is set by the + #: :envvar:`FLASK_ENV` environment variable and may not behave as + #: expected if set in code. + #: + #: **Do not enable development when deploying in production.** + #: + #: Default: ``'production'`` + env = ConfigAttribute("ENV") + + @property + def debug(self) -> bool: + """Whether debug mode is enabled. When using ``flask run`` to start + the development server, an interactive debugger will be shown for + unhandled exceptions, and the server will be reloaded when code + changes. This maps to the :data:`DEBUG` config key. This is + enabled when :attr:`env` is ``'development'`` and is overridden + by the ``FLASK_DEBUG`` environment variable. It may not behave as + expected if set in code. + + **Do not enable debug mode when deploying in production.** + + Default: ``True`` if :attr:`env` is ``'development'``, or + ``False`` otherwise. + """ + return self.config["DEBUG"] + + @debug.setter + def debug(self, value: bool) -> None: + self.config["DEBUG"] = value + self.jinja_env.auto_reload = self.templates_auto_reload + + def run( + self, + host: t.Optional[str] = None, + port: t.Optional[int] = None, + debug: t.Optional[bool] = None, + load_dotenv: bool = True, + **options: t.Any, + ) -> None: + """Runs the application on a local development server. + + Do not use ``run()`` in a production setting. It is not intended to + meet security and performance requirements for a production server. + Instead, see :doc:`/deploying/index` for WSGI server recommendations. + + If the :attr:`debug` flag is set the server will automatically reload + for code changes and show a debugger in case an exception happened. + + If you want to run the application in debug mode, but disable the + code execution on the interactive debugger, you can pass + ``use_evalex=False`` as parameter. This will keep the debugger's + traceback screen active, but disable code execution. + + It is not recommended to use this function for development with + automatic reloading as this is badly supported. Instead you should + be using the :command:`flask` command line script's ``run`` support. + + .. admonition:: Keep in Mind + + Flask will suppress any server error with a generic error page + unless it is in debug mode. As such to enable just the + interactive debugger without the code reloading, you have to + invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. + Setting ``use_debugger`` to ``True`` without being in debug mode + won't catch any exceptions because there won't be any to + catch. + + :param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to + have the server available externally as well. Defaults to + ``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config variable + if present. + :param port: the port of the webserver. Defaults to ``5000`` or the + port defined in the ``SERVER_NAME`` config variable if present. + :param debug: if given, enable or disable debug mode. See + :attr:`debug`. + :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` + files to set environment variables. Will also change the working + directory to the directory containing the first file found. + :param options: the options to be forwarded to the underlying Werkzeug + server. See :func:`werkzeug.serving.run_simple` for more + information. + + .. versionchanged:: 1.0 + If installed, python-dotenv will be used to load environment + variables from :file:`.env` and :file:`.flaskenv` files. + + If set, the :envvar:`FLASK_ENV` and :envvar:`FLASK_DEBUG` + environment variables will override :attr:`env` and + :attr:`debug`. + + Threaded mode is enabled by default. + + .. versionchanged:: 0.10 + The default port is now picked from the ``SERVER_NAME`` + variable. + """ + # Change this into a no-op if the server is invoked from the + # command line. Have a look at cli.py for more information. + if os.environ.get("FLASK_RUN_FROM_CLI") == "true": + from .debughelpers import explain_ignored_app_run + + explain_ignored_app_run() + return + + if get_load_dotenv(load_dotenv): + cli.load_dotenv() + + # if set, let env vars override previous values + if "FLASK_ENV" in os.environ: + self.env = get_env() + self.debug = get_debug_flag() + elif "FLASK_DEBUG" in os.environ: + self.debug = get_debug_flag() + + # debug passed to method overrides all other sources + if debug is not None: + self.debug = bool(debug) + + server_name = self.config.get("SERVER_NAME") + sn_host = sn_port = None + + if server_name: + sn_host, _, sn_port = server_name.partition(":") + + if not host: + if sn_host: + host = sn_host + else: + host = "127.0.0.1" + + if port or port == 0: + port = int(port) + elif sn_port: + port = int(sn_port) + else: + port = 5000 + + options.setdefault("use_reloader", self.debug) + options.setdefault("use_debugger", self.debug) + options.setdefault("threaded", True) + + cli.show_server_banner(self.env, self.debug, self.name, False) + + from werkzeug.serving import run_simple + + try: + run_simple(t.cast(str, host), port, self, **options) + finally: + # reset the first request information if the development server + # reset normally. This makes it possible to restart the server + # without reloader and that stuff from an interactive shell. + self._got_first_request = False + + def test_client(self, use_cookies: bool = True, **kwargs: t.Any) -> "FlaskClient": + """Creates a test client for this application. For information + about unit testing head over to :doc:`/testing`. + + Note that if you are testing for assertions or exceptions in your + application code, you must set ``app.testing = True`` in order for the + exceptions to propagate to the test client. Otherwise, the exception + will be handled by the application (not visible to the test client) and + the only indication of an AssertionError or other exception will be a + 500 status code response to the test client. See the :attr:`testing` + attribute. For example:: + + app.testing = True + client = app.test_client() + + The test client can be used in a ``with`` block to defer the closing down + of the context until the end of the ``with`` block. This is useful if + you want to access the context locals for testing:: + + with app.test_client() as c: + rv = c.get('/?vodka=42') + assert request.args['vodka'] == '42' + + Additionally, you may pass optional keyword arguments that will then + be passed to the application's :attr:`test_client_class` constructor. + For example:: + + from flask.testing import FlaskClient + + class CustomClient(FlaskClient): + def __init__(self, *args, **kwargs): + self._authentication = kwargs.pop("authentication") + super(CustomClient,self).__init__( *args, **kwargs) + + app.test_client_class = CustomClient + client = app.test_client(authentication='Basic ....') + + See :class:`~flask.testing.FlaskClient` for more information. + + .. versionchanged:: 0.4 + added support for ``with`` block usage for the client. + + .. versionadded:: 0.7 + The `use_cookies` parameter was added as well as the ability + to override the client to be used by setting the + :attr:`test_client_class` attribute. + + .. versionchanged:: 0.11 + Added `**kwargs` to support passing additional keyword arguments to + the constructor of :attr:`test_client_class`. + """ + cls = self.test_client_class + if cls is None: + from .testing import FlaskClient as cls # type: ignore + return cls( # type: ignore + self, self.response_class, use_cookies=use_cookies, **kwargs + ) + + def test_cli_runner(self, **kwargs: t.Any) -> "FlaskCliRunner": + """Create a CLI runner for testing CLI commands. + See :ref:`testing-cli`. + + Returns an instance of :attr:`test_cli_runner_class`, by default + :class:`~flask.testing.FlaskCliRunner`. The Flask app object is + passed as the first argument. + + .. versionadded:: 1.0 + """ + cls = self.test_cli_runner_class + + if cls is None: + from .testing import FlaskCliRunner as cls # type: ignore + + return cls(self, **kwargs) # type: ignore + + @setupmethod + def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None: + """Register a :class:`~flask.Blueprint` on the application. Keyword + arguments passed to this method will override the defaults set on the + blueprint. + + Calls the blueprint's :meth:`~flask.Blueprint.register` method after + recording the blueprint in the application's :attr:`blueprints`. + + :param blueprint: The blueprint to register. + :param url_prefix: Blueprint routes will be prefixed with this. + :param subdomain: Blueprint routes will match on this subdomain. + :param url_defaults: Blueprint routes will use these default values for + view arguments. + :param options: Additional keyword arguments are passed to + :class:`~flask.blueprints.BlueprintSetupState`. They can be + accessed in :meth:`~flask.Blueprint.record` callbacks. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + + .. versionadded:: 0.7 + """ + blueprint.register(self, options) + + def iter_blueprints(self) -> t.ValuesView["Blueprint"]: + """Iterates over all blueprints by the order they were registered. + + .. versionadded:: 0.11 + """ + return self.blueprints.values() + + @setupmethod + def add_url_rule( + self, + rule: str, + endpoint: t.Optional[str] = None, + view_func: t.Optional[t.Callable] = None, + provide_automatic_options: t.Optional[bool] = None, + **options: t.Any, + ) -> None: + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) # type: ignore + options["endpoint"] = endpoint + methods = options.pop("methods", None) + + # if the methods are not given and the view_func object knows its + # methods we can use that instead. If neither exists, we go with + # a tuple of only ``GET`` as default. + if methods is None: + methods = getattr(view_func, "methods", None) or ("GET",) + if isinstance(methods, str): + raise TypeError( + "Allowed methods must be a list of strings, for" + ' example: @app.route(..., methods=["POST"])' + ) + methods = {item.upper() for item in methods} + + # Methods that should always be added + required_methods = set(getattr(view_func, "required_methods", ())) + + # starting with Flask 0.8 the view_func object can disable and + # force-enable the automatic options handling. + if provide_automatic_options is None: + provide_automatic_options = getattr( + view_func, "provide_automatic_options", None + ) + + if provide_automatic_options is None: + if "OPTIONS" not in methods: + provide_automatic_options = True + required_methods.add("OPTIONS") + else: + provide_automatic_options = False + + # Add the required methods now. + methods |= required_methods + + rule = self.url_rule_class(rule, methods=methods, **options) + rule.provide_automatic_options = provide_automatic_options # type: ignore + + self.url_map.add(rule) + if view_func is not None: + old_func = self.view_functions.get(endpoint) + if old_func is not None and old_func != view_func: + raise AssertionError( + "View function mapping is overwriting an existing" + f" endpoint function: {endpoint}" + ) + self.view_functions[endpoint] = view_func + + @setupmethod + def template_filter( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]: + """A decorator that is used to register custom template filter. + You can specify a name for the filter, otherwise the function + name will be used. Example:: + + @app.template_filter() + def reverse(s): + return s[::-1] + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def decorator(f: TemplateFilterCallable) -> TemplateFilterCallable: + self.add_template_filter(f, name=name) + return f + + return decorator + + @setupmethod + def add_template_filter( + self, f: TemplateFilterCallable, name: t.Optional[str] = None + ) -> None: + """Register a custom template filter. Works exactly like the + :meth:`template_filter` decorator. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + self.jinja_env.filters[name or f.__name__] = f + + @setupmethod + def template_test( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]: + """A decorator that is used to register custom template test. + You can specify a name for the test, otherwise the function + name will be used. Example:: + + @app.template_test() + def is_prime(n): + if n == 2: + return True + for i in range(2, int(math.ceil(math.sqrt(n))) + 1): + if n % i == 0: + return False + return True + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def decorator(f: TemplateTestCallable) -> TemplateTestCallable: + self.add_template_test(f, name=name) + return f + + return decorator + + @setupmethod + def add_template_test( + self, f: TemplateTestCallable, name: t.Optional[str] = None + ) -> None: + """Register a custom template test. Works exactly like the + :meth:`template_test` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + self.jinja_env.tests[name or f.__name__] = f + + @setupmethod + def template_global( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]: + """A decorator that is used to register a custom template global function. + You can specify a name for the global function, otherwise the function + name will be used. Example:: + + @app.template_global() + def double(n): + return 2 * n + + .. versionadded:: 0.10 + + :param name: the optional name of the global function, otherwise the + function name will be used. + """ + + def decorator(f: TemplateGlobalCallable) -> TemplateGlobalCallable: + self.add_template_global(f, name=name) + return f + + return decorator + + @setupmethod + def add_template_global( + self, f: TemplateGlobalCallable, name: t.Optional[str] = None + ) -> None: + """Register a custom template global function. Works exactly like the + :meth:`template_global` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the global function, otherwise the + function name will be used. + """ + self.jinja_env.globals[name or f.__name__] = f + + @setupmethod + def before_first_request( + self, f: BeforeFirstRequestCallable + ) -> BeforeFirstRequestCallable: + """Registers a function to be run before the first request to this + instance of the application. + + The function will be called without any arguments and its return + value is ignored. + + .. versionadded:: 0.8 + """ + self.before_first_request_funcs.append(f) + return f + + @setupmethod + def teardown_appcontext(self, f: TeardownCallable) -> TeardownCallable: + """Registers a function to be called when the application context + ends. These functions are typically also called when the request + context is popped. + + Example:: + + ctx = app.app_context() + ctx.push() + ... + ctx.pop() + + When ``ctx.pop()`` is executed in the above example, the teardown + functions are called just before the app context moves from the + stack of active contexts. This becomes relevant if you are using + such constructs in tests. + + Since a request context typically also manages an application + context it would also be called when you pop a request context. + + When a teardown function was called because of an unhandled exception + it will be passed an error object. If an :meth:`errorhandler` is + registered, it will handle the exception and the teardown will not + receive it. + + The return values of teardown functions are ignored. + + .. versionadded:: 0.9 + """ + self.teardown_appcontext_funcs.append(f) + return f + + @setupmethod + def shell_context_processor(self, f: t.Callable) -> t.Callable: + """Registers a shell context processor function. + + .. versionadded:: 0.11 + """ + self.shell_context_processors.append(f) + return f + + def _find_error_handler( + self, e: Exception + ) -> t.Optional["ErrorHandlerCallable[Exception]"]: + """Return a registered error handler for an exception in this order: + blueprint handler for a specific code, app handler for a specific code, + blueprint handler for an exception class, app handler for an exception + class, or ``None`` if a suitable handler is not found. + """ + exc_class, code = self._get_exc_class_and_code(type(e)) + names = (*request.blueprints, None) + + for c in (code, None) if code is not None else (None,): + for name in names: + handler_map = self.error_handler_spec[name][c] + + if not handler_map: + continue + + for cls in exc_class.__mro__: + handler = handler_map.get(cls) + + if handler is not None: + return handler + return None + + def handle_http_exception( + self, e: HTTPException + ) -> t.Union[HTTPException, ResponseReturnValue]: + """Handles an HTTP exception. By default this will invoke the + registered error handlers and fall back to returning the + exception as response. + + .. versionchanged:: 1.0.3 + ``RoutingException``, used internally for actions such as + slash redirects during routing, is not passed to error + handlers. + + .. versionchanged:: 1.0 + Exceptions are looked up by code *and* by MRO, so + ``HTTPException`` subclasses can be handled with a catch-all + handler for the base ``HTTPException``. + + .. versionadded:: 0.3 + """ + # Proxy exceptions don't have error codes. We want to always return + # those unchanged as errors + if e.code is None: + return e + + # RoutingExceptions are used internally to trigger routing + # actions, such as slash redirects raising RequestRedirect. They + # are not raised or handled in user code. + if isinstance(e, RoutingException): + return e + + handler = self._find_error_handler(e) + if handler is None: + return e + return self.ensure_sync(handler)(e) + + def trap_http_exception(self, e: Exception) -> bool: + """Checks if an HTTP exception should be trapped or not. By default + this will return ``False`` for all exceptions except for a bad request + key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to ``True``. It + also returns ``True`` if ``TRAP_HTTP_EXCEPTIONS`` is set to ``True``. + + This is called for all HTTP exceptions raised by a view function. + If it returns ``True`` for any exception the error handler for this + exception is not called and it shows up as regular exception in the + traceback. This is helpful for debugging implicitly raised HTTP + exceptions. + + .. versionchanged:: 1.0 + Bad request errors are not trapped by default in debug mode. + + .. versionadded:: 0.8 + """ + if self.config["TRAP_HTTP_EXCEPTIONS"]: + return True + + trap_bad_request = self.config["TRAP_BAD_REQUEST_ERRORS"] + + # if unset, trap key errors in debug mode + if ( + trap_bad_request is None + and self.debug + and isinstance(e, BadRequestKeyError) + ): + return True + + if trap_bad_request: + return isinstance(e, BadRequest) + + return False + + def handle_user_exception( + self, e: Exception + ) -> t.Union[HTTPException, ResponseReturnValue]: + """This method is called whenever an exception occurs that + should be handled. A special case is :class:`~werkzeug + .exceptions.HTTPException` which is forwarded to the + :meth:`handle_http_exception` method. This function will either + return a response value or reraise the exception with the same + traceback. + + .. versionchanged:: 1.0 + Key errors raised from request data like ``form`` show the + bad key in debug mode rather than a generic bad request + message. + + .. versionadded:: 0.7 + """ + if isinstance(e, BadRequestKeyError) and ( + self.debug or self.config["TRAP_BAD_REQUEST_ERRORS"] + ): + e.show_exception = True + + if isinstance(e, HTTPException) and not self.trap_http_exception(e): + return self.handle_http_exception(e) + + handler = self._find_error_handler(e) + + if handler is None: + raise + + return self.ensure_sync(handler)(e) + + def handle_exception(self, e: Exception) -> Response: + """Handle an exception that did not have an error handler + associated with it, or that was raised from an error handler. + This always causes a 500 ``InternalServerError``. + + Always sends the :data:`got_request_exception` signal. + + If :attr:`propagate_exceptions` is ``True``, such as in debug + mode, the error will be re-raised so that the debugger can + display it. Otherwise, the original exception is logged, and + an :exc:`~werkzeug.exceptions.InternalServerError` is returned. + + If an error handler is registered for ``InternalServerError`` or + ``500``, it will be used. For consistency, the handler will + always receive the ``InternalServerError``. The original + unhandled exception is available as ``e.original_exception``. + + .. versionchanged:: 1.1.0 + Always passes the ``InternalServerError`` instance to the + handler, setting ``original_exception`` to the unhandled + error. + + .. versionchanged:: 1.1.0 + ``after_request`` functions and other finalization is done + even for the default 500 response when there is no handler. + + .. versionadded:: 0.3 + """ + exc_info = sys.exc_info() + got_request_exception.send(self, exception=e) + + if self.propagate_exceptions: + # Re-raise if called with an active exception, otherwise + # raise the passed in exception. + if exc_info[1] is e: + raise + + raise e + + self.log_exception(exc_info) + server_error: t.Union[InternalServerError, ResponseReturnValue] + server_error = InternalServerError(original_exception=e) + handler = self._find_error_handler(server_error) + + if handler is not None: + server_error = self.ensure_sync(handler)(server_error) + + return self.finalize_request(server_error, from_error_handler=True) + + def log_exception( + self, + exc_info: t.Union[ + t.Tuple[type, BaseException, TracebackType], t.Tuple[None, None, None] + ], + ) -> None: + """Logs an exception. This is called by :meth:`handle_exception` + if debugging is disabled and right before the handler is called. + The default implementation logs the exception as error on the + :attr:`logger`. + + .. versionadded:: 0.8 + """ + self.logger.error( + f"Exception on {request.path} [{request.method}]", exc_info=exc_info + ) + + def raise_routing_exception(self, request: Request) -> "te.NoReturn": + """Exceptions that are recording during routing are reraised with + this method. During debug we are not reraising redirect requests + for non ``GET``, ``HEAD``, or ``OPTIONS`` requests and we're raising + a different error instead to help debug situations. + + :internal: + """ + if ( + not self.debug + or not isinstance(request.routing_exception, RequestRedirect) + or request.method in ("GET", "HEAD", "OPTIONS") + ): + raise request.routing_exception # type: ignore + + from .debughelpers import FormDataRoutingRedirect + + raise FormDataRoutingRedirect(request) + + def dispatch_request(self) -> ResponseReturnValue: + """Does the request dispatching. Matches the URL and returns the + return value of the view or error handler. This does not have to + be a response object. In order to convert the return value to a + proper response object, call :func:`make_response`. + + .. versionchanged:: 0.7 + This no longer does the exception handling, this code was + moved to the new :meth:`full_dispatch_request`. + """ + req = _request_ctx_stack.top.request + if req.routing_exception is not None: + self.raise_routing_exception(req) + rule = req.url_rule + # if we provide automatic options for this URL and the + # request came with the OPTIONS method, reply automatically + if ( + getattr(rule, "provide_automatic_options", False) + and req.method == "OPTIONS" + ): + return self.make_default_options_response() + # otherwise dispatch to the handler for that endpoint + return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args) + + def full_dispatch_request(self) -> Response: + """Dispatches the request and on top of that performs request + pre and postprocessing as well as HTTP exception catching and + error handling. + + .. versionadded:: 0.7 + """ + self.try_trigger_before_first_request_functions() + try: + request_started.send(self) + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() + except Exception as e: + rv = self.handle_user_exception(e) + return self.finalize_request(rv) + + def finalize_request( + self, + rv: t.Union[ResponseReturnValue, HTTPException], + from_error_handler: bool = False, + ) -> Response: + """Given the return value from a view function this finalizes + the request by converting it into a response and invoking the + postprocessing functions. This is invoked for both normal + request dispatching as well as error handlers. + + Because this means that it might be called as a result of a + failure a special safe mode is available which can be enabled + with the `from_error_handler` flag. If enabled, failures in + response processing will be logged and otherwise ignored. + + :internal: + """ + response = self.make_response(rv) + try: + response = self.process_response(response) + request_finished.send(self, response=response) + except Exception: + if not from_error_handler: + raise + self.logger.exception( + "Request finalizing failed with an error while handling an error" + ) + return response + + def try_trigger_before_first_request_functions(self) -> None: + """Called before each request and will ensure that it triggers + the :attr:`before_first_request_funcs` and only exactly once per + application instance (which means process usually). + + :internal: + """ + if self._got_first_request: + return + with self._before_request_lock: + if self._got_first_request: + return + for func in self.before_first_request_funcs: + self.ensure_sync(func)() + self._got_first_request = True + + def make_default_options_response(self) -> Response: + """This method is called to create the default ``OPTIONS`` response. + This can be changed through subclassing to change the default + behavior of ``OPTIONS`` responses. + + .. versionadded:: 0.7 + """ + adapter = _request_ctx_stack.top.url_adapter + methods = adapter.allowed_methods() + rv = self.response_class() + rv.allow.update(methods) + return rv + + def should_ignore_error(self, error: t.Optional[BaseException]) -> bool: + """This is called to figure out if an error should be ignored + or not as far as the teardown system is concerned. If this + function returns ``True`` then the teardown handlers will not be + passed the error. + + .. versionadded:: 0.10 + """ + return False + + def ensure_sync(self, func: t.Callable) -> t.Callable: + """Ensure that the function is synchronous for WSGI workers. + Plain ``def`` functions are returned as-is. ``async def`` + functions are wrapped to run and wait for the response. + + Override this method to change how the app runs async views. + + .. versionadded:: 2.0 + """ + if iscoroutinefunction(func): + return self.async_to_sync(func) + + return func + + def async_to_sync( + self, func: t.Callable[..., t.Coroutine] + ) -> t.Callable[..., t.Any]: + """Return a sync function that will run the coroutine function. + + .. code-block:: python + + result = app.async_to_sync(func)(*args, **kwargs) + + Override this method to change how the app converts async code + to be synchronously callable. + + .. versionadded:: 2.0 + """ + try: + from asgiref.sync import async_to_sync as asgiref_async_to_sync + except ImportError: + raise RuntimeError( + "Install Flask with the 'async' extra in order to use async views." + ) from None + + # Check that Werkzeug isn't using its fallback ContextVar class. + if ContextVar.__module__ == "werkzeug.local": + raise RuntimeError( + "Async cannot be used with this combination of Python " + "and Greenlet versions." + ) + + return asgiref_async_to_sync(func) + + def make_response(self, rv: ResponseReturnValue) -> Response: + """Convert the return value from a view function to an instance of + :attr:`response_class`. + + :param rv: the return value from the view function. The view function + must return a response. Returning ``None``, or the view ending + without returning, is not allowed. The following types are allowed + for ``view_rv``: + + ``str`` + A response object is created with the string encoded to UTF-8 + as the body. + + ``bytes`` + A response object is created with the bytes as the body. + + ``dict`` + A dictionary that will be jsonify'd before being returned. + + ``tuple`` + Either ``(body, status, headers)``, ``(body, status)``, or + ``(body, headers)``, where ``body`` is any of the other types + allowed here, ``status`` is a string or an integer, and + ``headers`` is a dictionary or a list of ``(key, value)`` + tuples. If ``body`` is a :attr:`response_class` instance, + ``status`` overwrites the exiting value and ``headers`` are + extended. + + :attr:`response_class` + The object is returned unchanged. + + other :class:`~werkzeug.wrappers.Response` class + The object is coerced to :attr:`response_class`. + + :func:`callable` + The function is called as a WSGI application. The result is + used to create a response object. + + .. versionchanged:: 0.9 + Previously a tuple was interpreted as the arguments for the + response object. + """ + + status = headers = None + + # unpack tuple returns + if isinstance(rv, tuple): + len_rv = len(rv) + + # a 3-tuple is unpacked directly + if len_rv == 3: + rv, status, headers = rv + # decide if a 2-tuple has status or headers + elif len_rv == 2: + if isinstance(rv[1], (Headers, dict, tuple, list)): + rv, headers = rv + else: + rv, status = rv + # other sized tuples are not allowed + else: + raise TypeError( + "The view function did not return a valid response tuple." + " The tuple must have the form (body, status, headers)," + " (body, status), or (body, headers)." + ) + + # the body must not be None + if rv is None: + raise TypeError( + f"The view function for {request.endpoint!r} did not" + " return a valid response. The function either returned" + " None or ended without a return statement." + ) + + # make sure the body is an instance of the response class + if not isinstance(rv, self.response_class): + if isinstance(rv, (str, bytes, bytearray)): + # let the response class set the status and headers instead of + # waiting to do it manually, so that the class can handle any + # special logic + rv = self.response_class(rv, status=status, headers=headers) + status = headers = None + elif isinstance(rv, dict): + rv = jsonify(rv) + elif isinstance(rv, BaseResponse) or callable(rv): + # evaluate a WSGI callable, or coerce a different response + # class to the correct type + try: + rv = self.response_class.force_type(rv, request.environ) # type: ignore # noqa: B950 + except TypeError as e: + raise TypeError( + f"{e}\nThe view function did not return a valid" + " response. The return type must be a string," + " dict, tuple, Response instance, or WSGI" + f" callable, but it was a {type(rv).__name__}." + ).with_traceback(sys.exc_info()[2]) from None + else: + raise TypeError( + "The view function did not return a valid" + " response. The return type must be a string," + " dict, tuple, Response instance, or WSGI" + f" callable, but it was a {type(rv).__name__}." + ) + + rv = t.cast(Response, rv) + # prefer the status if it was provided + if status is not None: + if isinstance(status, (str, bytes, bytearray)): + rv.status = status # type: ignore + else: + rv.status_code = status + + # extend existing headers with provided headers + if headers: + rv.headers.update(headers) + + return rv + + def create_url_adapter( + self, request: t.Optional[Request] + ) -> t.Optional[MapAdapter]: + """Creates a URL adapter for the given request. The URL adapter + is created at a point where the request context is not yet set + up so the request is passed explicitly. + + .. versionadded:: 0.6 + + .. versionchanged:: 0.9 + This can now also be called without a request object when the + URL adapter is created for the application context. + + .. versionchanged:: 1.0 + :data:`SERVER_NAME` no longer implicitly enables subdomain + matching. Use :attr:`subdomain_matching` instead. + """ + if request is not None: + # If subdomain matching is disabled (the default), use the + # default subdomain in all cases. This should be the default + # in Werkzeug but it currently does not have that feature. + if not self.subdomain_matching: + subdomain = self.url_map.default_subdomain or None + else: + subdomain = None + + return self.url_map.bind_to_environ( + request.environ, + server_name=self.config["SERVER_NAME"], + subdomain=subdomain, + ) + # We need at the very least the server name to be set for this + # to work. + if self.config["SERVER_NAME"] is not None: + return self.url_map.bind( + self.config["SERVER_NAME"], + script_name=self.config["APPLICATION_ROOT"], + url_scheme=self.config["PREFERRED_URL_SCHEME"], + ) + + return None + + def inject_url_defaults(self, endpoint: str, values: dict) -> None: + """Injects the URL defaults for the given endpoint directly into + the values dictionary passed. This is used internally and + automatically called on URL building. + + .. versionadded:: 0.7 + """ + names: t.Iterable[t.Optional[str]] = (None,) + + # url_for may be called outside a request context, parse the + # passed endpoint instead of using request.blueprints. + if "." in endpoint: + names = chain( + names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0])) + ) + + for name in names: + if name in self.url_default_functions: + for func in self.url_default_functions[name]: + func(endpoint, values) + + def handle_url_build_error( + self, error: Exception, endpoint: str, values: dict + ) -> str: + """Handle :class:`~werkzeug.routing.BuildError` on + :meth:`url_for`. + """ + for handler in self.url_build_error_handlers: + try: + rv = handler(error, endpoint, values) + except BuildError as e: + # make error available outside except block + error = e + else: + if rv is not None: + return rv + + # Re-raise if called with an active exception, otherwise raise + # the passed in exception. + if error is sys.exc_info()[1]: + raise + + raise error + + def preprocess_request(self) -> t.Optional[ResponseReturnValue]: + """Called before the request is dispatched. Calls + :attr:`url_value_preprocessors` registered with the app and the + current blueprint (if any). Then calls :attr:`before_request_funcs` + registered with the app and the blueprint. + + If any :meth:`before_request` handler returns a non-None value, the + value is handled as if it was the return value from the view, and + further request handling is stopped. + """ + names = (None, *reversed(request.blueprints)) + + for name in names: + if name in self.url_value_preprocessors: + for url_func in self.url_value_preprocessors[name]: + url_func(request.endpoint, request.view_args) + + for name in names: + if name in self.before_request_funcs: + for before_func in self.before_request_funcs[name]: + rv = self.ensure_sync(before_func)() + + if rv is not None: + return rv + + return None + + def process_response(self, response: Response) -> Response: + """Can be overridden in order to modify the response object + before it's sent to the WSGI server. By default this will + call all the :meth:`after_request` decorated functions. + + .. versionchanged:: 0.5 + As of Flask 0.5 the functions registered for after request + execution are called in reverse order of registration. + + :param response: a :attr:`response_class` object. + :return: a new response object or the same, has to be an + instance of :attr:`response_class`. + """ + ctx = _request_ctx_stack.top + + for func in ctx._after_request_functions: + response = self.ensure_sync(func)(response) + + for name in chain(request.blueprints, (None,)): + if name in self.after_request_funcs: + for func in reversed(self.after_request_funcs[name]): + response = self.ensure_sync(func)(response) + + if not self.session_interface.is_null_session(ctx.session): + self.session_interface.save_session(self, ctx.session, response) + + return response + + def do_teardown_request( + self, exc: t.Optional[BaseException] = _sentinel # type: ignore + ) -> None: + """Called after the request is dispatched and the response is + returned, right before the request context is popped. + + This calls all functions decorated with + :meth:`teardown_request`, and :meth:`Blueprint.teardown_request` + if a blueprint handled the request. Finally, the + :data:`request_tearing_down` signal is sent. + + This is called by + :meth:`RequestContext.pop() `, + which may be delayed during testing to maintain access to + resources. + + :param exc: An unhandled exception raised while dispatching the + request. Detected from the current exception information if + not passed. Passed to each teardown function. + + .. versionchanged:: 0.9 + Added the ``exc`` argument. + """ + if exc is _sentinel: + exc = sys.exc_info()[1] + + for name in chain(request.blueprints, (None,)): + if name in self.teardown_request_funcs: + for func in reversed(self.teardown_request_funcs[name]): + self.ensure_sync(func)(exc) + + request_tearing_down.send(self, exc=exc) + + def do_teardown_appcontext( + self, exc: t.Optional[BaseException] = _sentinel # type: ignore + ) -> None: + """Called right before the application context is popped. + + When handling a request, the application context is popped + after the request context. See :meth:`do_teardown_request`. + + This calls all functions decorated with + :meth:`teardown_appcontext`. Then the + :data:`appcontext_tearing_down` signal is sent. + + This is called by + :meth:`AppContext.pop() `. + + .. versionadded:: 0.9 + """ + if exc is _sentinel: + exc = sys.exc_info()[1] + + for func in reversed(self.teardown_appcontext_funcs): + self.ensure_sync(func)(exc) + + appcontext_tearing_down.send(self, exc=exc) + + def app_context(self) -> AppContext: + """Create an :class:`~flask.ctx.AppContext`. Use as a ``with`` + block to push the context, which will make :data:`current_app` + point at this application. + + An application context is automatically pushed by + :meth:`RequestContext.push() ` + when handling a request, and when running a CLI command. Use + this to manually create a context outside of these situations. + + :: + + with app.app_context(): + init_db() + + See :doc:`/appcontext`. + + .. versionadded:: 0.9 + """ + return AppContext(self) + + def request_context(self, environ: dict) -> RequestContext: + """Create a :class:`~flask.ctx.RequestContext` representing a + WSGI environment. Use a ``with`` block to push the context, + which will make :data:`request` point at this request. + + See :doc:`/reqcontext`. + + Typically you should not call this from your own code. A request + context is automatically pushed by the :meth:`wsgi_app` when + handling a request. Use :meth:`test_request_context` to create + an environment and context instead of this method. + + :param environ: a WSGI environment + """ + return RequestContext(self, environ) + + def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext: + """Create a :class:`~flask.ctx.RequestContext` for a WSGI + environment created from the given values. This is mostly useful + during testing, where you may want to run a function that uses + request data without dispatching a full request. + + See :doc:`/reqcontext`. + + Use a ``with`` block to push the context, which will make + :data:`request` point at the request for the created + environment. :: + + with test_request_context(...): + generate_report() + + When using the shell, it may be easier to push and pop the + context manually to avoid indentation. :: + + ctx = app.test_request_context(...) + ctx.push() + ... + ctx.pop() + + Takes the same arguments as Werkzeug's + :class:`~werkzeug.test.EnvironBuilder`, with some defaults from + the application. See the linked Werkzeug docs for most of the + available arguments. Flask-specific behavior is listed here. + + :param path: URL path being requested. + :param base_url: Base URL where the app is being served, which + ``path`` is relative to. If not given, built from + :data:`PREFERRED_URL_SCHEME`, ``subdomain``, + :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. + :param subdomain: Subdomain name to append to + :data:`SERVER_NAME`. + :param url_scheme: Scheme to use instead of + :data:`PREFERRED_URL_SCHEME`. + :param data: The request body, either as a string or a dict of + form keys and values. + :param json: If given, this is serialized as JSON and passed as + ``data``. Also defaults ``content_type`` to + ``application/json``. + :param args: other positional arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + :param kwargs: other keyword arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + """ + from .testing import EnvironBuilder + + builder = EnvironBuilder(self, *args, **kwargs) + + try: + return self.request_context(builder.get_environ()) + finally: + builder.close() + + def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any: + """The actual WSGI application. This is not implemented in + :meth:`__call__` so that middlewares can be applied without + losing a reference to the app object. Instead of doing this:: + + app = MyMiddleware(app) + + It's a better idea to do this instead:: + + app.wsgi_app = MyMiddleware(app.wsgi_app) + + Then you still have the original application object around and + can continue to call methods on it. + + .. versionchanged:: 0.7 + Teardown events for the request and app contexts are called + even if an unhandled error occurs. Other events may not be + called depending on when an error occurs during dispatch. + See :ref:`callbacks-and-errors`. + + :param environ: A WSGI environment. + :param start_response: A callable accepting a status code, + a list of headers, and an optional exception context to + start the response. + """ + ctx = self.request_context(environ) + error: t.Optional[BaseException] = None + try: + try: + ctx.push() + response = self.full_dispatch_request() + except Exception as e: + error = e + response = self.handle_exception(e) + except: # noqa: B001 + error = sys.exc_info()[1] + raise + return response(environ, start_response) + finally: + if self.should_ignore_error(error): + error = None + ctx.auto_pop(error) + + def __call__(self, environ: dict, start_response: t.Callable) -> t.Any: + """The WSGI server calls the Flask application object as the + WSGI application. This calls :meth:`wsgi_app`, which can be + wrapped to apply middleware. + """ + return self.wsgi_app(environ, start_response) diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/blueprints.py b/flask-app/venv/lib/python3.8/site-packages/flask/blueprints.py new file mode 100644 index 000000000..5c23a735c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/flask/blueprints.py @@ -0,0 +1,609 @@ +import os +import typing as t +from collections import defaultdict +from functools import update_wrapper + +from .scaffold import _endpoint_from_view_func +from .scaffold import _sentinel +from .scaffold import Scaffold +from .typing import AfterRequestCallable +from .typing import BeforeFirstRequestCallable +from .typing import BeforeRequestCallable +from .typing import TeardownCallable +from .typing import TemplateContextProcessorCallable +from .typing import TemplateFilterCallable +from .typing import TemplateGlobalCallable +from .typing import TemplateTestCallable +from .typing import URLDefaultCallable +from .typing import URLValuePreprocessorCallable + +if t.TYPE_CHECKING: + from .app import Flask + from .typing import ErrorHandlerCallable + +DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable] + + +class BlueprintSetupState: + """Temporary holder object for registering a blueprint with the + application. An instance of this class is created by the + :meth:`~flask.Blueprint.make_setup_state` method and later passed + to all register callback functions. + """ + + def __init__( + self, + blueprint: "Blueprint", + app: "Flask", + options: t.Any, + first_registration: bool, + ) -> None: + #: a reference to the current application + self.app = app + + #: a reference to the blueprint that created this setup state. + self.blueprint = blueprint + + #: a dictionary with all options that were passed to the + #: :meth:`~flask.Flask.register_blueprint` method. + self.options = options + + #: as blueprints can be registered multiple times with the + #: application and not everything wants to be registered + #: multiple times on it, this attribute can be used to figure + #: out if the blueprint was registered in the past already. + self.first_registration = first_registration + + subdomain = self.options.get("subdomain") + if subdomain is None: + subdomain = self.blueprint.subdomain + + #: The subdomain that the blueprint should be active for, ``None`` + #: otherwise. + self.subdomain = subdomain + + url_prefix = self.options.get("url_prefix") + if url_prefix is None: + url_prefix = self.blueprint.url_prefix + #: The prefix that should be used for all URLs defined on the + #: blueprint. + self.url_prefix = url_prefix + + self.name = self.options.get("name", blueprint.name) + self.name_prefix = self.options.get("name_prefix", "") + + #: A dictionary with URL defaults that is added to each and every + #: URL that was defined with the blueprint. + self.url_defaults = dict(self.blueprint.url_values_defaults) + self.url_defaults.update(self.options.get("url_defaults", ())) + + def add_url_rule( + self, + rule: str, + endpoint: t.Optional[str] = None, + view_func: t.Optional[t.Callable] = None, + **options: t.Any, + ) -> None: + """A helper method to register a rule (and optionally a view function) + to the application. The endpoint is automatically prefixed with the + blueprint's name. + """ + if self.url_prefix is not None: + if rule: + rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/"))) + else: + rule = self.url_prefix + options.setdefault("subdomain", self.subdomain) + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) # type: ignore + defaults = self.url_defaults + if "defaults" in options: + defaults = dict(defaults, **options.pop("defaults")) + + self.app.add_url_rule( + rule, + f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."), + view_func, + defaults=defaults, + **options, + ) + + +class Blueprint(Scaffold): + """Represents a blueprint, a collection of routes and other + app-related functions that can be registered on a real application + later. + + A blueprint is an object that allows defining application functions + without requiring an application object ahead of time. It uses the + same decorators as :class:`~flask.Flask`, but defers the need for an + application by recording them for later registration. + + Decorating a function with a blueprint creates a deferred function + that is called with :class:`~flask.blueprints.BlueprintSetupState` + when the blueprint is registered on an application. + + See :doc:`/blueprints` for more information. + + :param name: The name of the blueprint. Will be prepended to each + endpoint name. + :param import_name: The name of the blueprint package, usually + ``__name__``. This helps locate the ``root_path`` for the + blueprint. + :param static_folder: A folder with static files that should be + served by the blueprint's static route. The path is relative to + the blueprint's root path. Blueprint static files are disabled + by default. + :param static_url_path: The url to serve static files from. + Defaults to ``static_folder``. If the blueprint does not have + a ``url_prefix``, the app's static route will take precedence, + and the blueprint's static files won't be accessible. + :param template_folder: A folder with templates that should be added + to the app's template search path. The path is relative to the + blueprint's root path. Blueprint templates are disabled by + default. Blueprint templates have a lower precedence than those + in the app's templates folder. + :param url_prefix: A path to prepend to all of the blueprint's URLs, + to make them distinct from the rest of the app's routes. + :param subdomain: A subdomain that blueprint routes will match on by + default. + :param url_defaults: A dict of default values that blueprint routes + will receive by default. + :param root_path: By default, the blueprint will automatically set + this based on ``import_name``. In certain situations this + automatic detection can fail, so the path can be specified + manually instead. + + .. versionchanged:: 1.1.0 + Blueprints have a ``cli`` group to register nested CLI commands. + The ``cli_group`` parameter controls the name of the group under + the ``flask`` command. + + .. versionadded:: 0.7 + """ + + warn_on_modifications = False + _got_registered_once = False + + #: Blueprint local JSON encoder class to use. Set to ``None`` to use + #: the app's :class:`~flask.Flask.json_encoder`. + json_encoder = None + #: Blueprint local JSON decoder class to use. Set to ``None`` to use + #: the app's :class:`~flask.Flask.json_decoder`. + json_decoder = None + + def __init__( + self, + name: str, + import_name: str, + static_folder: t.Optional[t.Union[str, os.PathLike]] = None, + static_url_path: t.Optional[str] = None, + template_folder: t.Optional[str] = None, + url_prefix: t.Optional[str] = None, + subdomain: t.Optional[str] = None, + url_defaults: t.Optional[dict] = None, + root_path: t.Optional[str] = None, + cli_group: t.Optional[str] = _sentinel, # type: ignore + ): + super().__init__( + import_name=import_name, + static_folder=static_folder, + static_url_path=static_url_path, + template_folder=template_folder, + root_path=root_path, + ) + + if "." in name: + raise ValueError("'name' may not contain a dot '.' character.") + + self.name = name + self.url_prefix = url_prefix + self.subdomain = subdomain + self.deferred_functions: t.List[DeferredSetupFunction] = [] + + if url_defaults is None: + url_defaults = {} + + self.url_values_defaults = url_defaults + self.cli_group = cli_group + self._blueprints: t.List[t.Tuple["Blueprint", dict]] = [] + + def _is_setup_finished(self) -> bool: + return self.warn_on_modifications and self._got_registered_once + + def record(self, func: t.Callable) -> None: + """Registers a function that is called when the blueprint is + registered on the application. This function is called with the + state as argument as returned by the :meth:`make_setup_state` + method. + """ + if self._got_registered_once and self.warn_on_modifications: + from warnings import warn + + warn( + Warning( + "The blueprint was already registered once but is" + " getting modified now. These changes will not show" + " up." + ) + ) + self.deferred_functions.append(func) + + def record_once(self, func: t.Callable) -> None: + """Works like :meth:`record` but wraps the function in another + function that will ensure the function is only called once. If the + blueprint is registered a second time on the application, the + function passed is not called. + """ + + def wrapper(state: BlueprintSetupState) -> None: + if state.first_registration: + func(state) + + return self.record(update_wrapper(wrapper, func)) + + def make_setup_state( + self, app: "Flask", options: dict, first_registration: bool = False + ) -> BlueprintSetupState: + """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` + object that is later passed to the register callback functions. + Subclasses can override this to return a subclass of the setup state. + """ + return BlueprintSetupState(self, app, options, first_registration) + + def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None: + """Register a :class:`~flask.Blueprint` on this blueprint. Keyword + arguments passed to this method will override the defaults set + on the blueprint. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + + .. versionadded:: 2.0 + """ + if blueprint is self: + raise ValueError("Cannot register a blueprint on itself") + self._blueprints.append((blueprint, options)) + + def register(self, app: "Flask", options: dict) -> None: + """Called by :meth:`Flask.register_blueprint` to register all + views and callbacks registered on the blueprint with the + application. Creates a :class:`.BlueprintSetupState` and calls + each :meth:`record` callback with it. + + :param app: The application this blueprint is being registered + with. + :param options: Keyword arguments forwarded from + :meth:`~Flask.register_blueprint`. + + .. versionchanged:: 2.0.1 + Nested blueprints are registered with their dotted name. + This allows different blueprints with the same name to be + nested at different locations. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + + .. versionchanged:: 2.0.1 + Registering the same blueprint with the same name multiple + times is deprecated and will become an error in Flask 2.1. + """ + name_prefix = options.get("name_prefix", "") + self_name = options.get("name", self.name) + name = f"{name_prefix}.{self_name}".lstrip(".") + + if name in app.blueprints: + existing_at = f" '{name}'" if self_name != name else "" + + if app.blueprints[name] is not self: + raise ValueError( + f"The name '{self_name}' is already registered for" + f" a different blueprint{existing_at}. Use 'name='" + " to provide a unique name." + ) + else: + import warnings + + warnings.warn( + f"The name '{self_name}' is already registered for" + f" this blueprint{existing_at}. Use 'name=' to" + " provide a unique name. This will become an error" + " in Flask 2.1.", + stacklevel=4, + ) + + first_bp_registration = not any(bp is self for bp in app.blueprints.values()) + first_name_registration = name not in app.blueprints + + app.blueprints[name] = self + self._got_registered_once = True + state = self.make_setup_state(app, options, first_bp_registration) + + if self.has_static_folder: + state.add_url_rule( + f"{self.static_url_path}/", + view_func=self.send_static_file, + endpoint="static", + ) + + # Merge blueprint data into parent. + if first_bp_registration or first_name_registration: + + def extend(bp_dict, parent_dict): + for key, values in bp_dict.items(): + key = name if key is None else f"{name}.{key}" + parent_dict[key].extend(values) + + for key, value in self.error_handler_spec.items(): + key = name if key is None else f"{name}.{key}" + value = defaultdict( + dict, + { + code: { + exc_class: func for exc_class, func in code_values.items() + } + for code, code_values in value.items() + }, + ) + app.error_handler_spec[key] = value + + for endpoint, func in self.view_functions.items(): + app.view_functions[endpoint] = func + + extend(self.before_request_funcs, app.before_request_funcs) + extend(self.after_request_funcs, app.after_request_funcs) + extend( + self.teardown_request_funcs, + app.teardown_request_funcs, + ) + extend(self.url_default_functions, app.url_default_functions) + extend(self.url_value_preprocessors, app.url_value_preprocessors) + extend(self.template_context_processors, app.template_context_processors) + + for deferred in self.deferred_functions: + deferred(state) + + cli_resolved_group = options.get("cli_group", self.cli_group) + + if self.cli.commands: + if cli_resolved_group is None: + app.cli.commands.update(self.cli.commands) + elif cli_resolved_group is _sentinel: + self.cli.name = name + app.cli.add_command(self.cli) + else: + self.cli.name = cli_resolved_group + app.cli.add_command(self.cli) + + for blueprint, bp_options in self._blueprints: + bp_options = bp_options.copy() + bp_url_prefix = bp_options.get("url_prefix") + + if bp_url_prefix is None: + bp_url_prefix = blueprint.url_prefix + + if state.url_prefix is not None and bp_url_prefix is not None: + bp_options["url_prefix"] = ( + state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/") + ) + elif bp_url_prefix is not None: + bp_options["url_prefix"] = bp_url_prefix + elif state.url_prefix is not None: + bp_options["url_prefix"] = state.url_prefix + + bp_options["name_prefix"] = name + blueprint.register(app, bp_options) + + def add_url_rule( + self, + rule: str, + endpoint: t.Optional[str] = None, + view_func: t.Optional[t.Callable] = None, + provide_automatic_options: t.Optional[bool] = None, + **options: t.Any, + ) -> None: + """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for + the :func:`url_for` function is prefixed with the name of the blueprint. + """ + if endpoint and "." in endpoint: + raise ValueError("'endpoint' may not contain a dot '.' character.") + + if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__: + raise ValueError("'view_func' name may not contain a dot '.' character.") + + self.record( + lambda s: s.add_url_rule( + rule, + endpoint, + view_func, + provide_automatic_options=provide_automatic_options, + **options, + ) + ) + + def app_template_filter( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]: + """Register a custom template filter, available application wide. Like + :meth:`Flask.template_filter` but for a blueprint. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def decorator(f: TemplateFilterCallable) -> TemplateFilterCallable: + self.add_app_template_filter(f, name=name) + return f + + return decorator + + def add_app_template_filter( + self, f: TemplateFilterCallable, name: t.Optional[str] = None + ) -> None: + """Register a custom template filter, available application wide. Like + :meth:`Flask.add_template_filter` but for a blueprint. Works exactly + like the :meth:`app_template_filter` decorator. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.filters[name or f.__name__] = f + + self.record_once(register_template) + + def app_template_test( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]: + """Register a custom template test, available application wide. Like + :meth:`Flask.template_test` but for a blueprint. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def decorator(f: TemplateTestCallable) -> TemplateTestCallable: + self.add_app_template_test(f, name=name) + return f + + return decorator + + def add_app_template_test( + self, f: TemplateTestCallable, name: t.Optional[str] = None + ) -> None: + """Register a custom template test, available application wide. Like + :meth:`Flask.add_template_test` but for a blueprint. Works exactly + like the :meth:`app_template_test` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.tests[name or f.__name__] = f + + self.record_once(register_template) + + def app_template_global( + self, name: t.Optional[str] = None + ) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]: + """Register a custom template global, available application wide. Like + :meth:`Flask.template_global` but for a blueprint. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + + def decorator(f: TemplateGlobalCallable) -> TemplateGlobalCallable: + self.add_app_template_global(f, name=name) + return f + + return decorator + + def add_app_template_global( + self, f: TemplateGlobalCallable, name: t.Optional[str] = None + ) -> None: + """Register a custom template global, available application wide. Like + :meth:`Flask.add_template_global` but for a blueprint. Works exactly + like the :meth:`app_template_global` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.globals[name or f.__name__] = f + + self.record_once(register_template) + + def before_app_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable: + """Like :meth:`Flask.before_request`. Such a function is executed + before each request, even if outside of a blueprint. + """ + self.record_once( + lambda s: s.app.before_request_funcs.setdefault(None, []).append(f) + ) + return f + + def before_app_first_request( + self, f: BeforeFirstRequestCallable + ) -> BeforeFirstRequestCallable: + """Like :meth:`Flask.before_first_request`. Such a function is + executed before the first request to the application. + """ + self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) + return f + + def after_app_request(self, f: AfterRequestCallable) -> AfterRequestCallable: + """Like :meth:`Flask.after_request` but for a blueprint. Such a function + is executed after each request, even if outside of the blueprint. + """ + self.record_once( + lambda s: s.app.after_request_funcs.setdefault(None, []).append(f) + ) + return f + + def teardown_app_request(self, f: TeardownCallable) -> TeardownCallable: + """Like :meth:`Flask.teardown_request` but for a blueprint. Such a + function is executed when tearing down each request, even if outside of + the blueprint. + """ + self.record_once( + lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f) + ) + return f + + def app_context_processor( + self, f: TemplateContextProcessorCallable + ) -> TemplateContextProcessorCallable: + """Like :meth:`Flask.context_processor` but for a blueprint. Such a + function is executed each request, even if outside of the blueprint. + """ + self.record_once( + lambda s: s.app.template_context_processors.setdefault(None, []).append(f) + ) + return f + + def app_errorhandler(self, code: t.Union[t.Type[Exception], int]) -> t.Callable: + """Like :meth:`Flask.errorhandler` but for a blueprint. This + handler is used for all requests, even if outside of the blueprint. + """ + + def decorator( + f: "ErrorHandlerCallable[Exception]", + ) -> "ErrorHandlerCallable[Exception]": + self.record_once(lambda s: s.app.errorhandler(code)(f)) + return f + + return decorator + + def app_url_value_preprocessor( + self, f: URLValuePreprocessorCallable + ) -> URLValuePreprocessorCallable: + """Same as :meth:`url_value_preprocessor` but application wide.""" + self.record_once( + lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f) + ) + return f + + def app_url_defaults(self, f: URLDefaultCallable) -> URLDefaultCallable: + """Same as :meth:`url_defaults` but application wide.""" + self.record_once( + lambda s: s.app.url_default_functions.setdefault(None, []).append(f) + ) + return f diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/cli.py b/flask-app/venv/lib/python3.8/site-packages/flask/cli.py new file mode 100644 index 000000000..7ab4fa1c9 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/flask/cli.py @@ -0,0 +1,998 @@ +import ast +import inspect +import os +import platform +import re +import sys +import traceback +import warnings +from functools import update_wrapper +from operator import attrgetter +from threading import Lock +from threading import Thread + +import click +from werkzeug.utils import import_string + +from .globals import current_app +from .helpers import get_debug_flag +from .helpers import get_env +from .helpers import get_load_dotenv + +try: + import dotenv +except ImportError: + dotenv = None + +try: + import ssl +except ImportError: + ssl = None # type: ignore + + +class NoAppException(click.UsageError): + """Raised if an application cannot be found or loaded.""" + + +def find_best_app(script_info, module): + """Given a module instance this tries to find the best possible + application in the module or raises an exception. + """ + from . import Flask + + # Search for the most common names first. + for attr_name in ("app", "application"): + app = getattr(module, attr_name, None) + + if isinstance(app, Flask): + return app + + # Otherwise find the only object that is a Flask instance. + matches = [v for v in module.__dict__.values() if isinstance(v, Flask)] + + if len(matches) == 1: + return matches[0] + elif len(matches) > 1: + raise NoAppException( + "Detected multiple Flask applications in module" + f" {module.__name__!r}. Use 'FLASK_APP={module.__name__}:name'" + f" to specify the correct one." + ) + + # Search for app factory functions. + for attr_name in ("create_app", "make_app"): + app_factory = getattr(module, attr_name, None) + + if inspect.isfunction(app_factory): + try: + app = call_factory(script_info, app_factory) + + if isinstance(app, Flask): + return app + except TypeError as e: + if not _called_with_wrong_args(app_factory): + raise + + raise NoAppException( + f"Detected factory {attr_name!r} in module {module.__name__!r}," + " but could not call it without arguments. Use" + f" \"FLASK_APP='{module.__name__}:{attr_name}(args)'\"" + " to specify arguments." + ) from e + + raise NoAppException( + "Failed to find Flask application or factory in module" + f" {module.__name__!r}. Use 'FLASK_APP={module.__name__}:name'" + " to specify one." + ) + + +def call_factory(script_info, app_factory, args=None, kwargs=None): + """Takes an app factory, a ``script_info` object and optionally a tuple + of arguments. Checks for the existence of a script_info argument and calls + the app_factory depending on that and the arguments provided. + """ + sig = inspect.signature(app_factory) + args = [] if args is None else args + kwargs = {} if kwargs is None else kwargs + + if "script_info" in sig.parameters: + warnings.warn( + "The 'script_info' argument is deprecated and will not be" + " passed to the app factory function in Flask 2.1.", + DeprecationWarning, + ) + kwargs["script_info"] = script_info + + if not args and len(sig.parameters) == 1: + first_parameter = next(iter(sig.parameters.values())) + + if ( + first_parameter.default is inspect.Parameter.empty + # **kwargs is reported as an empty default, ignore it + and first_parameter.kind is not inspect.Parameter.VAR_KEYWORD + ): + warnings.warn( + "Script info is deprecated and will not be passed as the" + " single argument to the app factory function in Flask" + " 2.1.", + DeprecationWarning, + ) + args.append(script_info) + + return app_factory(*args, **kwargs) + + +def _called_with_wrong_args(f): + """Check whether calling a function raised a ``TypeError`` because + the call failed or because something in the factory raised the + error. + + :param f: The function that was called. + :return: ``True`` if the call failed. + """ + tb = sys.exc_info()[2] + + try: + while tb is not None: + if tb.tb_frame.f_code is f.__code__: + # In the function, it was called successfully. + return False + + tb = tb.tb_next + + # Didn't reach the function. + return True + finally: + # Delete tb to break a circular reference. + # https://docs.python.org/2/library/sys.html#sys.exc_info + del tb + + +def find_app_by_string(script_info, module, app_name): + """Check if the given string is a variable name or a function. Call + a function to get the app instance, or return the variable directly. + """ + from . import Flask + + # Parse app_name as a single expression to determine if it's a valid + # attribute name or function call. + try: + expr = ast.parse(app_name.strip(), mode="eval").body + except SyntaxError: + raise NoAppException( + f"Failed to parse {app_name!r} as an attribute name or function call." + ) from None + + if isinstance(expr, ast.Name): + name = expr.id + args = kwargs = None + elif isinstance(expr, ast.Call): + # Ensure the function name is an attribute name only. + if not isinstance(expr.func, ast.Name): + raise NoAppException( + f"Function reference must be a simple name: {app_name!r}." + ) + + name = expr.func.id + + # Parse the positional and keyword arguments as literals. + try: + args = [ast.literal_eval(arg) for arg in expr.args] + kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expr.keywords} + except ValueError: + # literal_eval gives cryptic error messages, show a generic + # message with the full expression instead. + raise NoAppException( + f"Failed to parse arguments as literal values: {app_name!r}." + ) from None + else: + raise NoAppException( + f"Failed to parse {app_name!r} as an attribute name or function call." + ) + + try: + attr = getattr(module, name) + except AttributeError as e: + raise NoAppException( + f"Failed to find attribute {name!r} in {module.__name__!r}." + ) from e + + # If the attribute is a function, call it with any args and kwargs + # to get the real application. + if inspect.isfunction(attr): + try: + app = call_factory(script_info, attr, args, kwargs) + except TypeError as e: + if not _called_with_wrong_args(attr): + raise + + raise NoAppException( + f"The factory {app_name!r} in module" + f" {module.__name__!r} could not be called with the" + " specified arguments." + ) from e + else: + app = attr + + if isinstance(app, Flask): + return app + + raise NoAppException( + "A valid Flask application was not obtained from" + f" '{module.__name__}:{app_name}'." + ) + + +def prepare_import(path): + """Given a filename this will try to calculate the python path, add it + to the search path and return the actual module name that is expected. + """ + path = os.path.realpath(path) + + fname, ext = os.path.splitext(path) + if ext == ".py": + path = fname + + if os.path.basename(path) == "__init__": + path = os.path.dirname(path) + + module_name = [] + + # move up until outside package structure (no __init__.py) + while True: + path, name = os.path.split(path) + module_name.append(name) + + if not os.path.exists(os.path.join(path, "__init__.py")): + break + + if sys.path[0] != path: + sys.path.insert(0, path) + + return ".".join(module_name[::-1]) + + +def locate_app(script_info, module_name, app_name, raise_if_not_found=True): + __traceback_hide__ = True # noqa: F841 + + try: + __import__(module_name) + except ImportError as e: + # Reraise the ImportError if it occurred within the imported module. + # Determine this by checking whether the trace has a depth > 1. + if sys.exc_info()[2].tb_next: + raise NoAppException( + f"While importing {module_name!r}, an ImportError was raised." + ) from e + elif raise_if_not_found: + raise NoAppException(f"Could not import {module_name!r}.") from e + else: + return + + module = sys.modules[module_name] + + if app_name is None: + return find_best_app(script_info, module) + else: + return find_app_by_string(script_info, module, app_name) + + +def get_version(ctx, param, value): + if not value or ctx.resilient_parsing: + return + + import werkzeug + from . import __version__ + + click.echo( + f"Python {platform.python_version()}\n" + f"Flask {__version__}\n" + f"Werkzeug {werkzeug.__version__}", + color=ctx.color, + ) + ctx.exit() + + +version_option = click.Option( + ["--version"], + help="Show the flask version", + expose_value=False, + callback=get_version, + is_flag=True, + is_eager=True, +) + + +class DispatchingApp: + """Special application that dispatches to a Flask application which + is imported by name in a background thread. If an error happens + it is recorded and shown as part of the WSGI handling which in case + of the Werkzeug debugger means that it shows up in the browser. + """ + + def __init__(self, loader, use_eager_loading=None): + self.loader = loader + self._app = None + self._lock = Lock() + self._bg_loading_exc = None + + if use_eager_loading is None: + use_eager_loading = os.environ.get("WERKZEUG_RUN_MAIN") != "true" + + if use_eager_loading: + self._load_unlocked() + else: + self._load_in_background() + + def _load_in_background(self): + def _load_app(): + __traceback_hide__ = True # noqa: F841 + with self._lock: + try: + self._load_unlocked() + except Exception as e: + self._bg_loading_exc = e + + t = Thread(target=_load_app, args=()) + t.start() + + def _flush_bg_loading_exception(self): + __traceback_hide__ = True # noqa: F841 + exc = self._bg_loading_exc + + if exc is not None: + self._bg_loading_exc = None + raise exc + + def _load_unlocked(self): + __traceback_hide__ = True # noqa: F841 + self._app = rv = self.loader() + self._bg_loading_exc = None + return rv + + def __call__(self, environ, start_response): + __traceback_hide__ = True # noqa: F841 + if self._app is not None: + return self._app(environ, start_response) + self._flush_bg_loading_exception() + with self._lock: + if self._app is not None: + rv = self._app + else: + rv = self._load_unlocked() + return rv(environ, start_response) + + +class ScriptInfo: + """Helper object to deal with Flask applications. This is usually not + necessary to interface with as it's used internally in the dispatching + to click. In future versions of Flask this object will most likely play + a bigger role. Typically it's created automatically by the + :class:`FlaskGroup` but you can also manually create it and pass it + onwards as click object. + """ + + def __init__(self, app_import_path=None, create_app=None, set_debug_flag=True): + #: Optionally the import path for the Flask application. + self.app_import_path = app_import_path or os.environ.get("FLASK_APP") + #: Optionally a function that is passed the script info to create + #: the instance of the application. + self.create_app = create_app + #: A dictionary with arbitrary data that can be associated with + #: this script info. + self.data = {} + self.set_debug_flag = set_debug_flag + self._loaded_app = None + + def load_app(self): + """Loads the Flask app (if not yet loaded) and returns it. Calling + this multiple times will just result in the already loaded app to + be returned. + """ + __traceback_hide__ = True # noqa: F841 + + if self._loaded_app is not None: + return self._loaded_app + + if self.create_app is not None: + app = call_factory(self, self.create_app) + else: + if self.app_import_path: + path, name = ( + re.split(r":(?![\\/])", self.app_import_path, 1) + [None] + )[:2] + import_name = prepare_import(path) + app = locate_app(self, import_name, name) + else: + for path in ("wsgi.py", "app.py"): + import_name = prepare_import(path) + app = locate_app(self, import_name, None, raise_if_not_found=False) + + if app: + break + + if not app: + raise NoAppException( + "Could not locate a Flask application. You did not provide " + 'the "FLASK_APP" environment variable, and a "wsgi.py" or ' + '"app.py" module was not found in the current directory.' + ) + + if self.set_debug_flag: + # Update the app's debug flag through the descriptor so that + # other values repopulate as well. + app.debug = get_debug_flag() + + self._loaded_app = app + return app + + +pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) + + +def with_appcontext(f): + """Wraps a callback so that it's guaranteed to be executed with the + script's application context. If callbacks are registered directly + to the ``app.cli`` object then they are wrapped with this function + by default unless it's disabled. + """ + + @click.pass_context + def decorator(__ctx, *args, **kwargs): + with __ctx.ensure_object(ScriptInfo).load_app().app_context(): + return __ctx.invoke(f, *args, **kwargs) + + return update_wrapper(decorator, f) + + +class AppGroup(click.Group): + """This works similar to a regular click :class:`~click.Group` but it + changes the behavior of the :meth:`command` decorator so that it + automatically wraps the functions in :func:`with_appcontext`. + + Not to be confused with :class:`FlaskGroup`. + """ + + def command(self, *args, **kwargs): + """This works exactly like the method of the same name on a regular + :class:`click.Group` but it wraps callbacks in :func:`with_appcontext` + unless it's disabled by passing ``with_appcontext=False``. + """ + wrap_for_ctx = kwargs.pop("with_appcontext", True) + + def decorator(f): + if wrap_for_ctx: + f = with_appcontext(f) + return click.Group.command(self, *args, **kwargs)(f) + + return decorator + + def group(self, *args, **kwargs): + """This works exactly like the method of the same name on a regular + :class:`click.Group` but it defaults the group class to + :class:`AppGroup`. + """ + kwargs.setdefault("cls", AppGroup) + return click.Group.group(self, *args, **kwargs) + + +class FlaskGroup(AppGroup): + """Special subclass of the :class:`AppGroup` group that supports + loading more commands from the configured Flask app. Normally a + developer does not have to interface with this class but there are + some very advanced use cases for which it makes sense to create an + instance of this. see :ref:`custom-scripts`. + + :param add_default_commands: if this is True then the default run and + shell commands will be added. + :param add_version_option: adds the ``--version`` option. + :param create_app: an optional callback that is passed the script info and + returns the loaded app. + :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` + files to set environment variables. Will also change the working + directory to the directory containing the first file found. + :param set_debug_flag: Set the app's debug flag based on the active + environment + + .. versionchanged:: 1.0 + If installed, python-dotenv will be used to load environment variables + from :file:`.env` and :file:`.flaskenv` files. + """ + + def __init__( + self, + add_default_commands=True, + create_app=None, + add_version_option=True, + load_dotenv=True, + set_debug_flag=True, + **extra, + ): + params = list(extra.pop("params", None) or ()) + + if add_version_option: + params.append(version_option) + + AppGroup.__init__(self, params=params, **extra) + self.create_app = create_app + self.load_dotenv = load_dotenv + self.set_debug_flag = set_debug_flag + + if add_default_commands: + self.add_command(run_command) + self.add_command(shell_command) + self.add_command(routes_command) + + self._loaded_plugin_commands = False + + def _load_plugin_commands(self): + if self._loaded_plugin_commands: + return + try: + import pkg_resources + except ImportError: + self._loaded_plugin_commands = True + return + + for ep in pkg_resources.iter_entry_points("flask.commands"): + self.add_command(ep.load(), ep.name) + self._loaded_plugin_commands = True + + def get_command(self, ctx, name): + self._load_plugin_commands() + # Look up built-in and plugin commands, which should be + # available even if the app fails to load. + rv = super().get_command(ctx, name) + + if rv is not None: + return rv + + info = ctx.ensure_object(ScriptInfo) + + # Look up commands provided by the app, showing an error and + # continuing if the app couldn't be loaded. + try: + return info.load_app().cli.get_command(ctx, name) + except NoAppException as e: + click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") + + def list_commands(self, ctx): + self._load_plugin_commands() + # Start with the built-in and plugin commands. + rv = set(super().list_commands(ctx)) + info = ctx.ensure_object(ScriptInfo) + + # Add commands provided by the app, showing an error and + # continuing if the app couldn't be loaded. + try: + rv.update(info.load_app().cli.list_commands(ctx)) + except NoAppException as e: + # When an app couldn't be loaded, show the error message + # without the traceback. + click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") + except Exception: + # When any other errors occurred during loading, show the + # full traceback. + click.secho(f"{traceback.format_exc()}\n", err=True, fg="red") + + return sorted(rv) + + def main(self, *args, **kwargs): + # Set a global flag that indicates that we were invoked from the + # command line interface. This is detected by Flask.run to make the + # call into a no-op. This is necessary to avoid ugly errors when the + # script that is loaded here also attempts to start a server. + os.environ["FLASK_RUN_FROM_CLI"] = "true" + + if get_load_dotenv(self.load_dotenv): + load_dotenv() + + obj = kwargs.get("obj") + + if obj is None: + obj = ScriptInfo( + create_app=self.create_app, set_debug_flag=self.set_debug_flag + ) + + kwargs["obj"] = obj + kwargs.setdefault("auto_envvar_prefix", "FLASK") + return super().main(*args, **kwargs) + + +def _path_is_ancestor(path, other): + """Take ``other`` and remove the length of ``path`` from it. Then join it + to ``path``. If it is the original value, ``path`` is an ancestor of + ``other``.""" + return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other + + +def load_dotenv(path=None): + """Load "dotenv" files in order of precedence to set environment variables. + + If an env var is already set it is not overwritten, so earlier files in the + list are preferred over later files. + + This is a no-op if `python-dotenv`_ is not installed. + + .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme + + :param path: Load the file at this location instead of searching. + :return: ``True`` if a file was loaded. + + .. versionchanged:: 1.1.0 + Returns ``False`` when python-dotenv is not installed, or when + the given path isn't a file. + + .. versionchanged:: 2.0 + When loading the env files, set the default encoding to UTF-8. + + .. versionadded:: 1.0 + """ + if dotenv is None: + if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"): + click.secho( + " * Tip: There are .env or .flaskenv files present." + ' Do "pip install python-dotenv" to use them.', + fg="yellow", + err=True, + ) + + return False + + # if the given path specifies the actual file then return True, + # else False + if path is not None: + if os.path.isfile(path): + return dotenv.load_dotenv(path, encoding="utf-8") + + return False + + new_dir = None + + for name in (".env", ".flaskenv"): + path = dotenv.find_dotenv(name, usecwd=True) + + if not path: + continue + + if new_dir is None: + new_dir = os.path.dirname(path) + + dotenv.load_dotenv(path, encoding="utf-8") + + return new_dir is not None # at least one file was located and loaded + + +def show_server_banner(env, debug, app_import_path, eager_loading): + """Show extra startup messages the first time the server is run, + ignoring the reloader. + """ + if os.environ.get("WERKZEUG_RUN_MAIN") == "true": + return + + if app_import_path is not None: + message = f" * Serving Flask app {app_import_path!r}" + + if not eager_loading: + message += " (lazy loading)" + + click.echo(message) + + click.echo(f" * Environment: {env}") + + if env == "production": + click.secho( + " WARNING: This is a development server. Do not use it in" + " a production deployment.", + fg="red", + ) + click.secho(" Use a production WSGI server instead.", dim=True) + + if debug is not None: + click.echo(f" * Debug mode: {'on' if debug else 'off'}") + + +class CertParamType(click.ParamType): + """Click option type for the ``--cert`` option. Allows either an + existing file, the string ``'adhoc'``, or an import for a + :class:`~ssl.SSLContext` object. + """ + + name = "path" + + def __init__(self): + self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True) + + def convert(self, value, param, ctx): + if ssl is None: + raise click.BadParameter( + 'Using "--cert" requires Python to be compiled with SSL support.', + ctx, + param, + ) + + try: + return self.path_type(value, param, ctx) + except click.BadParameter: + value = click.STRING(value, param, ctx).lower() + + if value == "adhoc": + try: + import cryptography # noqa: F401 + except ImportError: + raise click.BadParameter( + "Using ad-hoc certificates requires the cryptography library.", + ctx, + param, + ) from None + + return value + + obj = import_string(value, silent=True) + + if isinstance(obj, ssl.SSLContext): + return obj + + raise + + +def _validate_key(ctx, param, value): + """The ``--key`` option must be specified when ``--cert`` is a file. + Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed. + """ + cert = ctx.params.get("cert") + is_adhoc = cert == "adhoc" + is_context = ssl and isinstance(cert, ssl.SSLContext) + + if value is not None: + if is_adhoc: + raise click.BadParameter( + 'When "--cert" is "adhoc", "--key" is not used.', ctx, param + ) + + if is_context: + raise click.BadParameter( + 'When "--cert" is an SSLContext object, "--key is not used.', ctx, param + ) + + if not cert: + raise click.BadParameter('"--cert" must also be specified.', ctx, param) + + ctx.params["cert"] = cert, value + + else: + if cert and not (is_adhoc or is_context): + raise click.BadParameter('Required when using "--cert".', ctx, param) + + return value + + +class SeparatedPathType(click.Path): + """Click option type that accepts a list of values separated by the + OS's path separator (``:``, ``;`` on Windows). Each value is + validated as a :class:`click.Path` type. + """ + + def convert(self, value, param, ctx): + items = self.split_envvar_value(value) + super_convert = super().convert + return [super_convert(item, param, ctx) for item in items] + + +@click.command("run", short_help="Run a development server.") +@click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.") +@click.option("--port", "-p", default=5000, help="The port to bind to.") +@click.option( + "--cert", type=CertParamType(), help="Specify a certificate file to use HTTPS." +) +@click.option( + "--key", + type=click.Path(exists=True, dir_okay=False, resolve_path=True), + callback=_validate_key, + expose_value=False, + help="The key file to use when specifying a certificate.", +) +@click.option( + "--reload/--no-reload", + default=None, + help="Enable or disable the reloader. By default the reloader " + "is active if debug is enabled.", +) +@click.option( + "--debugger/--no-debugger", + default=None, + help="Enable or disable the debugger. By default the debugger " + "is active if debug is enabled.", +) +@click.option( + "--eager-loading/--lazy-loading", + default=None, + help="Enable or disable eager loading. By default eager " + "loading is enabled if the reloader is disabled.", +) +@click.option( + "--with-threads/--without-threads", + default=True, + help="Enable or disable multithreading.", +) +@click.option( + "--extra-files", + default=None, + type=SeparatedPathType(), + help=( + "Extra files that trigger a reload on change. Multiple paths" + f" are separated by {os.path.pathsep!r}." + ), +) +@pass_script_info +def run_command( + info, host, port, reload, debugger, eager_loading, with_threads, cert, extra_files +): + """Run a local development server. + + This server is for development purposes only. It does not provide + the stability, security, or performance of production WSGI servers. + + The reloader and debugger are enabled by default if + FLASK_ENV=development or FLASK_DEBUG=1. + """ + debug = get_debug_flag() + + if reload is None: + reload = debug + + if debugger is None: + debugger = debug + + show_server_banner(get_env(), debug, info.app_import_path, eager_loading) + app = DispatchingApp(info.load_app, use_eager_loading=eager_loading) + + from werkzeug.serving import run_simple + + run_simple( + host, + port, + app, + use_reloader=reload, + use_debugger=debugger, + threaded=with_threads, + ssl_context=cert, + extra_files=extra_files, + ) + + +@click.command("shell", short_help="Run a shell in the app context.") +@with_appcontext +def shell_command() -> None: + """Run an interactive Python shell in the context of a given + Flask application. The application will populate the default + namespace of this shell according to its configuration. + + This is useful for executing small snippets of management code + without having to manually configure the application. + """ + import code + from .globals import _app_ctx_stack + + app = _app_ctx_stack.top.app + banner = ( + f"Python {sys.version} on {sys.platform}\n" + f"App: {app.import_name} [{app.env}]\n" + f"Instance: {app.instance_path}" + ) + ctx: dict = {} + + # Support the regular Python interpreter startup script if someone + # is using it. + startup = os.environ.get("PYTHONSTARTUP") + if startup and os.path.isfile(startup): + with open(startup) as f: + eval(compile(f.read(), startup, "exec"), ctx) + + ctx.update(app.make_shell_context()) + + # Site, customize, or startup script can set a hook to call when + # entering interactive mode. The default one sets up readline with + # tab and history completion. + interactive_hook = getattr(sys, "__interactivehook__", None) + + if interactive_hook is not None: + try: + import readline + from rlcompleter import Completer + except ImportError: + pass + else: + # rlcompleter uses __main__.__dict__ by default, which is + # flask.__main__. Use the shell context instead. + readline.set_completer(Completer(ctx).complete) + + interactive_hook() + + code.interact(banner=banner, local=ctx) + + +@click.command("routes", short_help="Show the routes for the app.") +@click.option( + "--sort", + "-s", + type=click.Choice(("endpoint", "methods", "rule", "match")), + default="endpoint", + help=( + 'Method to sort routes by. "match" is the order that Flask will match ' + "routes when dispatching a request." + ), +) +@click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.") +@with_appcontext +def routes_command(sort: str, all_methods: bool) -> None: + """Show all registered routes with endpoints and methods.""" + + rules = list(current_app.url_map.iter_rules()) + if not rules: + click.echo("No routes were registered.") + return + + ignored_methods = set(() if all_methods else ("HEAD", "OPTIONS")) + + if sort in ("endpoint", "rule"): + rules = sorted(rules, key=attrgetter(sort)) + elif sort == "methods": + rules = sorted(rules, key=lambda rule: sorted(rule.methods)) # type: ignore + + rule_methods = [ + ", ".join(sorted(rule.methods - ignored_methods)) # type: ignore + for rule in rules + ] + + headers = ("Endpoint", "Methods", "Rule") + widths = ( + max(len(rule.endpoint) for rule in rules), + max(len(methods) for methods in rule_methods), + max(len(rule.rule) for rule in rules), + ) + widths = [max(len(h), w) for h, w in zip(headers, widths)] + row = "{{0:<{0}}} {{1:<{1}}} {{2:<{2}}}".format(*widths) + + click.echo(row.format(*headers).strip()) + click.echo(row.format(*("-" * width for width in widths))) + + for rule, methods in zip(rules, rule_methods): + click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip()) + + +cli = FlaskGroup( + help="""\ +A general utility script for Flask applications. + +Provides commands from Flask, extensions, and the application. Loads the +application defined in the FLASK_APP environment variable, or from a wsgi.py +file. Setting the FLASK_ENV environment variable to 'development' will enable +debug mode. + +\b + {prefix}{cmd} FLASK_APP=hello.py + {prefix}{cmd} FLASK_ENV=development + {prefix}flask run +""".format( + cmd="export" if os.name == "posix" else "set", + prefix="$ " if os.name == "posix" else "> ", + ) +) + + +def main() -> None: + if int(click.__version__[0]) < 8: + warnings.warn( + "Using the `flask` cli with Click 7 is deprecated and" + " will not be supported starting with Flask 2.1." + " Please upgrade to Click 8 as soon as possible.", + DeprecationWarning, + ) + # TODO omit sys.argv once https://github.com/pallets/click/issues/536 is fixed + cli.main(args=sys.argv[1:]) + + +if __name__ == "__main__": + main() diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/config.py b/flask-app/venv/lib/python3.8/site-packages/flask/config.py new file mode 100644 index 000000000..ca769022f --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/flask/config.py @@ -0,0 +1,295 @@ +import errno +import os +import types +import typing as t + +from werkzeug.utils import import_string + + +class ConfigAttribute: + """Makes an attribute forward to the config""" + + def __init__(self, name: str, get_converter: t.Optional[t.Callable] = None) -> None: + self.__name__ = name + self.get_converter = get_converter + + def __get__(self, obj: t.Any, owner: t.Any = None) -> t.Any: + if obj is None: + return self + rv = obj.config[self.__name__] + if self.get_converter is not None: + rv = self.get_converter(rv) + return rv + + def __set__(self, obj: t.Any, value: t.Any) -> None: + obj.config[self.__name__] = value + + +class Config(dict): + """Works exactly like a dict but provides ways to fill it from files + or special dictionaries. There are two common patterns to populate the + config. + + Either you can fill the config from a config file:: + + app.config.from_pyfile('yourconfig.cfg') + + Or alternatively you can define the configuration options in the + module that calls :meth:`from_object` or provide an import path to + a module that should be loaded. It is also possible to tell it to + use the same module and with that provide the configuration values + just before the call:: + + DEBUG = True + SECRET_KEY = 'development key' + app.config.from_object(__name__) + + In both cases (loading from any Python file or loading from modules), + only uppercase keys are added to the config. This makes it possible to use + lowercase values in the config file for temporary values that are not added + to the config or to define the config keys in the same file that implements + the application. + + Probably the most interesting way to load configurations is from an + environment variable pointing to a file:: + + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + + In this case before launching the application you have to set this + environment variable to the file you want to use. On Linux and OS X + use the export statement:: + + export YOURAPPLICATION_SETTINGS='/path/to/config/file' + + On windows use `set` instead. + + :param root_path: path to which files are read relative from. When the + config object is created by the application, this is + the application's :attr:`~flask.Flask.root_path`. + :param defaults: an optional dictionary of default values + """ + + def __init__(self, root_path: str, defaults: t.Optional[dict] = None) -> None: + dict.__init__(self, defaults or {}) + self.root_path = root_path + + def from_envvar(self, variable_name: str, silent: bool = False) -> bool: + """Loads a configuration from an environment variable pointing to + a configuration file. This is basically just a shortcut with nicer + error messages for this line of code:: + + app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) + + :param variable_name: name of the environment variable + :param silent: set to ``True`` if you want silent failure for missing + files. + :return: ``True`` if the file was loaded successfully. + """ + rv = os.environ.get(variable_name) + if not rv: + if silent: + return False + raise RuntimeError( + f"The environment variable {variable_name!r} is not set" + " and as such configuration could not be loaded. Set" + " this variable and make it point to a configuration" + " file" + ) + return self.from_pyfile(rv, silent=silent) + + def from_pyfile(self, filename: str, silent: bool = False) -> bool: + """Updates the values in the config from a Python file. This function + behaves as if the file was imported as module with the + :meth:`from_object` function. + + :param filename: the filename of the config. This can either be an + absolute filename or a filename relative to the + root path. + :param silent: set to ``True`` if you want silent failure for missing + files. + :return: ``True`` if the file was loaded successfully. + + .. versionadded:: 0.7 + `silent` parameter. + """ + filename = os.path.join(self.root_path, filename) + d = types.ModuleType("config") + d.__file__ = filename + try: + with open(filename, mode="rb") as config_file: + exec(compile(config_file.read(), filename, "exec"), d.__dict__) + except OSError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR): + return False + e.strerror = f"Unable to load configuration file ({e.strerror})" + raise + self.from_object(d) + return True + + def from_object(self, obj: t.Union[object, str]) -> None: + """Updates the values from the given object. An object can be of one + of the following two types: + + - a string: in this case the object with that name will be imported + - an actual object reference: that object is used directly + + Objects are usually either modules or classes. :meth:`from_object` + loads only the uppercase attributes of the module/class. A ``dict`` + object will not work with :meth:`from_object` because the keys of a + ``dict`` are not attributes of the ``dict`` class. + + Example of module-based configuration:: + + app.config.from_object('yourapplication.default_config') + from yourapplication import default_config + app.config.from_object(default_config) + + Nothing is done to the object before loading. If the object is a + class and has ``@property`` attributes, it needs to be + instantiated before being passed to this method. + + You should not use this function to load the actual configuration but + rather configuration defaults. The actual config should be loaded + with :meth:`from_pyfile` and ideally from a location not within the + package because the package might be installed system wide. + + See :ref:`config-dev-prod` for an example of class-based configuration + using :meth:`from_object`. + + :param obj: an import name or object + """ + if isinstance(obj, str): + obj = import_string(obj) + for key in dir(obj): + if key.isupper(): + self[key] = getattr(obj, key) + + def from_file( + self, + filename: str, + load: t.Callable[[t.IO[t.Any]], t.Mapping], + silent: bool = False, + ) -> bool: + """Update the values in the config from a file that is loaded + using the ``load`` parameter. The loaded data is passed to the + :meth:`from_mapping` method. + + .. code-block:: python + + import toml + app.config.from_file("config.toml", load=toml.load) + + :param filename: The path to the data file. This can be an + absolute path or relative to the config root path. + :param load: A callable that takes a file handle and returns a + mapping of loaded data from the file. + :type load: ``Callable[[Reader], Mapping]`` where ``Reader`` + implements a ``read`` method. + :param silent: Ignore the file if it doesn't exist. + :return: ``True`` if the file was loaded successfully. + + .. versionadded:: 2.0 + """ + filename = os.path.join(self.root_path, filename) + + try: + with open(filename) as f: + obj = load(f) + except OSError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR): + return False + + e.strerror = f"Unable to load configuration file ({e.strerror})" + raise + + return self.from_mapping(obj) + + def from_json(self, filename: str, silent: bool = False) -> bool: + """Update the values in the config from a JSON file. The loaded + data is passed to the :meth:`from_mapping` method. + + :param filename: The path to the JSON file. This can be an + absolute path or relative to the config root path. + :param silent: Ignore the file if it doesn't exist. + :return: ``True`` if the file was loaded successfully. + + .. deprecated:: 2.0.0 + Will be removed in Flask 2.1. Use :meth:`from_file` instead. + This was removed early in 2.0.0, was added back in 2.0.1. + + .. versionadded:: 0.11 + """ + import warnings + from . import json + + warnings.warn( + "'from_json' is deprecated and will be removed in Flask" + " 2.1. Use 'from_file(path, json.load)' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.from_file(filename, json.load, silent=silent) + + def from_mapping( + self, mapping: t.Optional[t.Mapping[str, t.Any]] = None, **kwargs: t.Any + ) -> bool: + """Updates the config like :meth:`update` ignoring items with non-upper + keys. + :return: Always returns ``True``. + + .. versionadded:: 0.11 + """ + mappings: t.Dict[str, t.Any] = {} + if mapping is not None: + mappings.update(mapping) + mappings.update(kwargs) + for key, value in mappings.items(): + if key.isupper(): + self[key] = value + return True + + def get_namespace( + self, namespace: str, lowercase: bool = True, trim_namespace: bool = True + ) -> t.Dict[str, t.Any]: + """Returns a dictionary containing a subset of configuration options + that match the specified namespace/prefix. Example usage:: + + app.config['IMAGE_STORE_TYPE'] = 'fs' + app.config['IMAGE_STORE_PATH'] = '/var/app/images' + app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com' + image_store_config = app.config.get_namespace('IMAGE_STORE_') + + The resulting dictionary `image_store_config` would look like:: + + { + 'type': 'fs', + 'path': '/var/app/images', + 'base_url': 'http://img.website.com' + } + + This is often useful when configuration options map directly to + keyword arguments in functions or class constructors. + + :param namespace: a configuration namespace + :param lowercase: a flag indicating if the keys of the resulting + dictionary should be lowercase + :param trim_namespace: a flag indicating if the keys of the resulting + dictionary should not include the namespace + + .. versionadded:: 0.11 + """ + rv = {} + for k, v in self.items(): + if not k.startswith(namespace): + continue + if trim_namespace: + key = k[len(namespace) :] + else: + key = k + if lowercase: + key = key.lower() + rv[key] = v + return rv + + def __repr__(self) -> str: + return f"<{type(self).__name__} {dict.__repr__(self)}>" diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/ctx.py b/flask-app/venv/lib/python3.8/site-packages/flask/ctx.py new file mode 100644 index 000000000..5c0646352 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/flask/ctx.py @@ -0,0 +1,480 @@ +import sys +import typing as t +from functools import update_wrapper +from types import TracebackType + +from werkzeug.exceptions import HTTPException + +from .globals import _app_ctx_stack +from .globals import _request_ctx_stack +from .signals import appcontext_popped +from .signals import appcontext_pushed +from .typing import AfterRequestCallable + +if t.TYPE_CHECKING: + from .app import Flask + from .sessions import SessionMixin + from .wrappers import Request + + +# a singleton sentinel value for parameter defaults +_sentinel = object() + + +class _AppCtxGlobals: + """A plain object. Used as a namespace for storing data during an + application context. + + Creating an app context automatically creates this object, which is + made available as the :data:`g` proxy. + + .. describe:: 'key' in g + + Check whether an attribute is present. + + .. versionadded:: 0.10 + + .. describe:: iter(g) + + Return an iterator over the attribute names. + + .. versionadded:: 0.10 + """ + + # Define attr methods to let mypy know this is a namespace object + # that has arbitrary attributes. + + def __getattr__(self, name: str) -> t.Any: + try: + return self.__dict__[name] + except KeyError: + raise AttributeError(name) from None + + def __setattr__(self, name: str, value: t.Any) -> None: + self.__dict__[name] = value + + def __delattr__(self, name: str) -> None: + try: + del self.__dict__[name] + except KeyError: + raise AttributeError(name) from None + + def get(self, name: str, default: t.Optional[t.Any] = None) -> t.Any: + """Get an attribute by name, or a default value. Like + :meth:`dict.get`. + + :param name: Name of attribute to get. + :param default: Value to return if the attribute is not present. + + .. versionadded:: 0.10 + """ + return self.__dict__.get(name, default) + + def pop(self, name: str, default: t.Any = _sentinel) -> t.Any: + """Get and remove an attribute by name. Like :meth:`dict.pop`. + + :param name: Name of attribute to pop. + :param default: Value to return if the attribute is not present, + instead of raising a ``KeyError``. + + .. versionadded:: 0.11 + """ + if default is _sentinel: + return self.__dict__.pop(name) + else: + return self.__dict__.pop(name, default) + + def setdefault(self, name: str, default: t.Any = None) -> t.Any: + """Get the value of an attribute if it is present, otherwise + set and return a default value. Like :meth:`dict.setdefault`. + + :param name: Name of attribute to get. + :param default: Value to set and return if the attribute is not + present. + + .. versionadded:: 0.11 + """ + return self.__dict__.setdefault(name, default) + + def __contains__(self, item: str) -> bool: + return item in self.__dict__ + + def __iter__(self) -> t.Iterator[str]: + return iter(self.__dict__) + + def __repr__(self) -> str: + top = _app_ctx_stack.top + if top is not None: + return f"" + return object.__repr__(self) + + +def after_this_request(f: AfterRequestCallable) -> AfterRequestCallable: + """Executes a function after this request. This is useful to modify + response objects. The function is passed the response object and has + to return the same or a new one. + + Example:: + + @app.route('/') + def index(): + @after_this_request + def add_header(response): + response.headers['X-Foo'] = 'Parachute' + return response + return 'Hello World!' + + This is more useful if a function other than the view function wants to + modify a response. For instance think of a decorator that wants to add + some headers without converting the return value into a response object. + + .. versionadded:: 0.9 + """ + _request_ctx_stack.top._after_request_functions.append(f) + return f + + +def copy_current_request_context(f: t.Callable) -> t.Callable: + """A helper function that decorates a function to retain the current + request context. This is useful when working with greenlets. The moment + the function is decorated a copy of the request context is created and + then pushed when the function is called. The current session is also + included in the copied request context. + + Example:: + + import gevent + from flask import copy_current_request_context + + @app.route('/') + def index(): + @copy_current_request_context + def do_some_work(): + # do some work here, it can access flask.request or + # flask.session like you would otherwise in the view function. + ... + gevent.spawn(do_some_work) + return 'Regular response' + + .. versionadded:: 0.10 + """ + top = _request_ctx_stack.top + if top is None: + raise RuntimeError( + "This decorator can only be used at local scopes " + "when a request context is on the stack. For instance within " + "view functions." + ) + reqctx = top.copy() + + def wrapper(*args, **kwargs): + with reqctx: + return f(*args, **kwargs) + + return update_wrapper(wrapper, f) + + +def has_request_context() -> bool: + """If you have code that wants to test if a request context is there or + not this function can be used. For instance, you may want to take advantage + of request information if the request object is available, but fail + silently if it is unavailable. + + :: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and has_request_context(): + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + Alternatively you can also just test any of the context bound objects + (such as :class:`request` or :class:`g`) for truthness:: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and request: + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + .. versionadded:: 0.7 + """ + return _request_ctx_stack.top is not None + + +def has_app_context() -> bool: + """Works like :func:`has_request_context` but for the application + context. You can also just do a boolean check on the + :data:`current_app` object instead. + + .. versionadded:: 0.9 + """ + return _app_ctx_stack.top is not None + + +class AppContext: + """The application context binds an application object implicitly + to the current thread or greenlet, similar to how the + :class:`RequestContext` binds request information. The application + context is also implicitly created if a request context is created + but the application is not on top of the individual application + context. + """ + + def __init__(self, app: "Flask") -> None: + self.app = app + self.url_adapter = app.create_url_adapter(None) + self.g = app.app_ctx_globals_class() + + # Like request context, app contexts can be pushed multiple times + # but there a basic "refcount" is enough to track them. + self._refcnt = 0 + + def push(self) -> None: + """Binds the app context to the current context.""" + self._refcnt += 1 + _app_ctx_stack.push(self) + appcontext_pushed.send(self.app) + + def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore + """Pops the app context.""" + try: + self._refcnt -= 1 + if self._refcnt <= 0: + if exc is _sentinel: + exc = sys.exc_info()[1] + self.app.do_teardown_appcontext(exc) + finally: + rv = _app_ctx_stack.pop() + assert rv is self, f"Popped wrong app context. ({rv!r} instead of {self!r})" + appcontext_popped.send(self.app) + + def __enter__(self) -> "AppContext": + self.push() + return self + + def __exit__( + self, exc_type: type, exc_value: BaseException, tb: TracebackType + ) -> None: + self.pop(exc_value) + + +class RequestContext: + """The request context contains all request relevant information. It is + created at the beginning of the request and pushed to the + `_request_ctx_stack` and removed at the end of it. It will create the + URL adapter and request object for the WSGI environment provided. + + Do not attempt to use this class directly, instead use + :meth:`~flask.Flask.test_request_context` and + :meth:`~flask.Flask.request_context` to create this object. + + When the request context is popped, it will evaluate all the + functions registered on the application for teardown execution + (:meth:`~flask.Flask.teardown_request`). + + The request context is automatically popped at the end of the request + for you. In debug mode the request context is kept around if + exceptions happen so that interactive debuggers have a chance to + introspect the data. With 0.4 this can also be forced for requests + that did not fail and outside of ``DEBUG`` mode. By setting + ``'flask._preserve_context'`` to ``True`` on the WSGI environment the + context will not pop itself at the end of the request. This is used by + the :meth:`~flask.Flask.test_client` for example to implement the + deferred cleanup functionality. + + You might find this helpful for unittests where you need the + information from the context local around for a little longer. Make + sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in + that situation, otherwise your unittests will leak memory. + """ + + def __init__( + self, + app: "Flask", + environ: dict, + request: t.Optional["Request"] = None, + session: t.Optional["SessionMixin"] = None, + ) -> None: + self.app = app + if request is None: + request = app.request_class(environ) + self.request = request + self.url_adapter = None + try: + self.url_adapter = app.create_url_adapter(self.request) + except HTTPException as e: + self.request.routing_exception = e + self.flashes = None + self.session = session + + # Request contexts can be pushed multiple times and interleaved with + # other request contexts. Now only if the last level is popped we + # get rid of them. Additionally if an application context is missing + # one is created implicitly so for each level we add this information + self._implicit_app_ctx_stack: t.List[t.Optional["AppContext"]] = [] + + # indicator if the context was preserved. Next time another context + # is pushed the preserved context is popped. + self.preserved = False + + # remembers the exception for pop if there is one in case the context + # preservation kicks in. + self._preserved_exc = None + + # Functions that should be executed after the request on the response + # object. These will be called before the regular "after_request" + # functions. + self._after_request_functions: t.List[AfterRequestCallable] = [] + + @property + def g(self) -> AppContext: + return _app_ctx_stack.top.g + + @g.setter + def g(self, value: AppContext) -> None: + _app_ctx_stack.top.g = value + + def copy(self) -> "RequestContext": + """Creates a copy of this request context with the same request object. + This can be used to move a request context to a different greenlet. + Because the actual request object is the same this cannot be used to + move a request context to a different thread unless access to the + request object is locked. + + .. versionadded:: 0.10 + + .. versionchanged:: 1.1 + The current session object is used instead of reloading the original + data. This prevents `flask.session` pointing to an out-of-date object. + """ + return self.__class__( + self.app, + environ=self.request.environ, + request=self.request, + session=self.session, + ) + + def match_request(self) -> None: + """Can be overridden by a subclass to hook into the matching + of the request. + """ + try: + result = self.url_adapter.match(return_rule=True) # type: ignore + self.request.url_rule, self.request.view_args = result # type: ignore + except HTTPException as e: + self.request.routing_exception = e + + def push(self) -> None: + """Binds the request context to the current context.""" + # If an exception occurs in debug mode or if context preservation is + # activated under exception situations exactly one context stays + # on the stack. The rationale is that you want to access that + # information under debug situations. However if someone forgets to + # pop that context again we want to make sure that on the next push + # it's invalidated, otherwise we run at risk that something leaks + # memory. This is usually only a problem in test suite since this + # functionality is not active in production environments. + top = _request_ctx_stack.top + if top is not None and top.preserved: + top.pop(top._preserved_exc) + + # Before we push the request context we have to ensure that there + # is an application context. + app_ctx = _app_ctx_stack.top + if app_ctx is None or app_ctx.app != self.app: + app_ctx = self.app.app_context() + app_ctx.push() + self._implicit_app_ctx_stack.append(app_ctx) + else: + self._implicit_app_ctx_stack.append(None) + + _request_ctx_stack.push(self) + + # Open the session at the moment that the request context is available. + # This allows a custom open_session method to use the request context. + # Only open a new session if this is the first time the request was + # pushed, otherwise stream_with_context loses the session. + if self.session is None: + session_interface = self.app.session_interface + self.session = session_interface.open_session(self.app, self.request) + + if self.session is None: + self.session = session_interface.make_null_session(self.app) + + # Match the request URL after loading the session, so that the + # session is available in custom URL converters. + if self.url_adapter is not None: + self.match_request() + + def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore + """Pops the request context and unbinds it by doing that. This will + also trigger the execution of functions registered by the + :meth:`~flask.Flask.teardown_request` decorator. + + .. versionchanged:: 0.9 + Added the `exc` argument. + """ + app_ctx = self._implicit_app_ctx_stack.pop() + clear_request = False + + try: + if not self._implicit_app_ctx_stack: + self.preserved = False + self._preserved_exc = None + if exc is _sentinel: + exc = sys.exc_info()[1] + self.app.do_teardown_request(exc) + + request_close = getattr(self.request, "close", None) + if request_close is not None: + request_close() + clear_request = True + finally: + rv = _request_ctx_stack.pop() + + # get rid of circular dependencies at the end of the request + # so that we don't require the GC to be active. + if clear_request: + rv.request.environ["werkzeug.request"] = None + + # Get rid of the app as well if necessary. + if app_ctx is not None: + app_ctx.pop(exc) + + assert ( + rv is self + ), f"Popped wrong request context. ({rv!r} instead of {self!r})" + + def auto_pop(self, exc: t.Optional[BaseException]) -> None: + if self.request.environ.get("flask._preserve_context") or ( + exc is not None and self.app.preserve_context_on_exception + ): + self.preserved = True + self._preserved_exc = exc # type: ignore + else: + self.pop(exc) + + def __enter__(self) -> "RequestContext": + self.push() + return self + + def __exit__( + self, exc_type: type, exc_value: BaseException, tb: TracebackType + ) -> None: + # do not pop the request stack if we are in debug mode and an + # exception happened. This will allow the debugger to still + # access the request object in the interactive shell. Furthermore + # the context can be force kept alive for the test client. + # See flask.testing for how this works. + self.auto_pop(exc_value) + + def __repr__(self) -> str: + return ( + f"<{type(self).__name__} {self.request.url!r}" + f" [{self.request.method}] of {self.app.name}>" + ) diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/debughelpers.py b/flask-app/venv/lib/python3.8/site-packages/flask/debughelpers.py new file mode 100644 index 000000000..212f7d7ee --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/flask/debughelpers.py @@ -0,0 +1,172 @@ +import os +import typing as t +from warnings import warn + +from .app import Flask +from .blueprints import Blueprint +from .globals import _request_ctx_stack + + +class UnexpectedUnicodeError(AssertionError, UnicodeError): + """Raised in places where we want some better error reporting for + unexpected unicode or binary data. + """ + + +class DebugFilesKeyError(KeyError, AssertionError): + """Raised from request.files during debugging. The idea is that it can + provide a better error message than just a generic KeyError/BadRequest. + """ + + def __init__(self, request, key): + form_matches = request.form.getlist(key) + buf = [ + f"You tried to access the file {key!r} in the request.files" + " dictionary but it does not exist. The mimetype for the" + f" request is {request.mimetype!r} instead of" + " 'multipart/form-data' which means that no file contents" + " were transmitted. To fix this error you should provide" + ' enctype="multipart/form-data" in your form.' + ] + if form_matches: + names = ", ".join(repr(x) for x in form_matches) + buf.append( + "\n\nThe browser instead transmitted some file names. " + f"This was submitted: {names}" + ) + self.msg = "".join(buf) + + def __str__(self): + return self.msg + + +class FormDataRoutingRedirect(AssertionError): + """This exception is raised by Flask in debug mode if it detects a + redirect caused by the routing system when the request method is not + GET, HEAD or OPTIONS. Reasoning: form data will be dropped. + """ + + def __init__(self, request): + exc = request.routing_exception + buf = [ + f"A request was sent to this URL ({request.url}) but a" + " redirect was issued automatically by the routing system" + f" to {exc.new_url!r}." + ] + + # In case just a slash was appended we can be extra helpful + if f"{request.base_url}/" == exc.new_url.split("?")[0]: + buf.append( + " The URL was defined with a trailing slash so Flask" + " will automatically redirect to the URL with the" + " trailing slash if it was accessed without one." + ) + + buf.append( + " Make sure to directly send your" + f" {request.method}-request to this URL since we can't make" + " browsers or HTTP clients redirect with form data reliably" + " or without user interaction." + ) + buf.append("\n\nNote: this exception is only raised in debug mode") + AssertionError.__init__(self, "".join(buf).encode("utf-8")) + + +def attach_enctype_error_multidict(request): + """Since Flask 0.8 we're monkeypatching the files object in case a + request is detected that does not use multipart form data but the files + object is accessed. + """ + oldcls = request.files.__class__ + + class newcls(oldcls): + def __getitem__(self, key): + try: + return oldcls.__getitem__(self, key) + except KeyError as e: + if key not in request.form: + raise + + raise DebugFilesKeyError(request, key) from e + + newcls.__name__ = oldcls.__name__ + newcls.__module__ = oldcls.__module__ + request.files.__class__ = newcls + + +def _dump_loader_info(loader) -> t.Generator: + yield f"class: {type(loader).__module__}.{type(loader).__name__}" + for key, value in sorted(loader.__dict__.items()): + if key.startswith("_"): + continue + if isinstance(value, (tuple, list)): + if not all(isinstance(x, str) for x in value): + continue + yield f"{key}:" + for item in value: + yield f" - {item}" + continue + elif not isinstance(value, (str, int, float, bool)): + continue + yield f"{key}: {value!r}" + + +def explain_template_loading_attempts(app: Flask, template, attempts) -> None: + """This should help developers understand what failed""" + info = [f"Locating template {template!r}:"] + total_found = 0 + blueprint = None + reqctx = _request_ctx_stack.top + if reqctx is not None and reqctx.request.blueprint is not None: + blueprint = reqctx.request.blueprint + + for idx, (loader, srcobj, triple) in enumerate(attempts): + if isinstance(srcobj, Flask): + src_info = f"application {srcobj.import_name!r}" + elif isinstance(srcobj, Blueprint): + src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})" + else: + src_info = repr(srcobj) + + info.append(f"{idx + 1:5}: trying loader of {src_info}") + + for line in _dump_loader_info(loader): + info.append(f" {line}") + + if triple is None: + detail = "no match" + else: + detail = f"found ({triple[1] or ''!r})" + total_found += 1 + info.append(f" -> {detail}") + + seems_fishy = False + if total_found == 0: + info.append("Error: the template could not be found.") + seems_fishy = True + elif total_found > 1: + info.append("Warning: multiple loaders returned a match for the template.") + seems_fishy = True + + if blueprint is not None and seems_fishy: + info.append( + " The template was looked up from an endpoint that belongs" + f" to the blueprint {blueprint!r}." + ) + info.append(" Maybe you did not place a template in the right folder?") + info.append(" See https://flask.palletsprojects.com/blueprints/#templates") + + app.logger.info("\n".join(info)) + + +def explain_ignored_app_run() -> None: + if os.environ.get("WERKZEUG_RUN_MAIN") != "true": + warn( + Warning( + "Silently ignoring app.run() because the application is" + " run from the flask command line executable. Consider" + ' putting app.run() behind an if __name__ == "__main__"' + " guard to silence this warning." + ), + stacklevel=3, + ) diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/globals.py b/flask-app/venv/lib/python3.8/site-packages/flask/globals.py new file mode 100644 index 000000000..6d91c75ed --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/flask/globals.py @@ -0,0 +1,59 @@ +import typing as t +from functools import partial + +from werkzeug.local import LocalProxy +from werkzeug.local import LocalStack + +if t.TYPE_CHECKING: + from .app import Flask + from .ctx import _AppCtxGlobals + from .sessions import SessionMixin + from .wrappers import Request + +_request_ctx_err_msg = """\ +Working outside of request context. + +This typically means that you attempted to use functionality that needed +an active HTTP request. Consult the documentation on testing for +information about how to avoid this problem.\ +""" +_app_ctx_err_msg = """\ +Working outside of application context. + +This typically means that you attempted to use functionality that needed +to interface with the current application object in some way. To solve +this, set up an application context with app.app_context(). See the +documentation for more information.\ +""" + + +def _lookup_req_object(name): + top = _request_ctx_stack.top + if top is None: + raise RuntimeError(_request_ctx_err_msg) + return getattr(top, name) + + +def _lookup_app_object(name): + top = _app_ctx_stack.top + if top is None: + raise RuntimeError(_app_ctx_err_msg) + return getattr(top, name) + + +def _find_app(): + top = _app_ctx_stack.top + if top is None: + raise RuntimeError(_app_ctx_err_msg) + return top.app + + +# context locals +_request_ctx_stack = LocalStack() +_app_ctx_stack = LocalStack() +current_app: "Flask" = LocalProxy(_find_app) # type: ignore +request: "Request" = LocalProxy(partial(_lookup_req_object, "request")) # type: ignore +session: "SessionMixin" = LocalProxy( # type: ignore + partial(_lookup_req_object, "session") +) +g: "_AppCtxGlobals" = LocalProxy(partial(_lookup_app_object, "g")) # type: ignore diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/helpers.py b/flask-app/venv/lib/python3.8/site-packages/flask/helpers.py new file mode 100644 index 000000000..7b8b08702 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/flask/helpers.py @@ -0,0 +1,836 @@ +import os +import pkgutil +import socket +import sys +import typing as t +import warnings +from datetime import datetime +from datetime import timedelta +from functools import lru_cache +from functools import update_wrapper +from threading import RLock + +import werkzeug.utils +from werkzeug.exceptions import NotFound +from werkzeug.routing import BuildError +from werkzeug.urls import url_quote + +from .globals import _app_ctx_stack +from .globals import _request_ctx_stack +from .globals import current_app +from .globals import request +from .globals import session +from .signals import message_flashed + +if t.TYPE_CHECKING: + from .wrappers import Response + + +def get_env() -> str: + """Get the environment the app is running in, indicated by the + :envvar:`FLASK_ENV` environment variable. The default is + ``'production'``. + """ + return os.environ.get("FLASK_ENV") or "production" + + +def get_debug_flag() -> bool: + """Get whether debug mode should be enabled for the app, indicated + by the :envvar:`FLASK_DEBUG` environment variable. The default is + ``True`` if :func:`.get_env` returns ``'development'``, or ``False`` + otherwise. + """ + val = os.environ.get("FLASK_DEBUG") + + if not val: + return get_env() == "development" + + return val.lower() not in ("0", "false", "no") + + +def get_load_dotenv(default: bool = True) -> bool: + """Get whether the user has disabled loading dotenv files by setting + :envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load the + files. + + :param default: What to return if the env var isn't set. + """ + val = os.environ.get("FLASK_SKIP_DOTENV") + + if not val: + return default + + return val.lower() in ("0", "false", "no") + + +def stream_with_context( + generator_or_function: t.Union[ + t.Iterator[t.AnyStr], t.Callable[..., t.Iterator[t.AnyStr]] + ] +) -> t.Iterator[t.AnyStr]: + """Request contexts disappear when the response is started on the server. + This is done for efficiency reasons and to make it less likely to encounter + memory leaks with badly written WSGI middlewares. The downside is that if + you are using streamed responses, the generator cannot access request bound + information any more. + + This function however can help you keep the context around for longer:: + + from flask import stream_with_context, request, Response + + @app.route('/stream') + def streamed_response(): + @stream_with_context + def generate(): + yield 'Hello ' + yield request.args['name'] + yield '!' + return Response(generate()) + + Alternatively it can also be used around a specific generator:: + + from flask import stream_with_context, request, Response + + @app.route('/stream') + def streamed_response(): + def generate(): + yield 'Hello ' + yield request.args['name'] + yield '!' + return Response(stream_with_context(generate())) + + .. versionadded:: 0.9 + """ + try: + gen = iter(generator_or_function) # type: ignore + except TypeError: + + def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any: + gen = generator_or_function(*args, **kwargs) # type: ignore + return stream_with_context(gen) + + return update_wrapper(decorator, generator_or_function) # type: ignore + + def generator() -> t.Generator: + ctx = _request_ctx_stack.top + if ctx is None: + raise RuntimeError( + "Attempted to stream with context but " + "there was no context in the first place to keep around." + ) + with ctx: + # Dummy sentinel. Has to be inside the context block or we're + # not actually keeping the context around. + yield None + + # The try/finally is here so that if someone passes a WSGI level + # iterator in we're still running the cleanup logic. Generators + # don't need that because they are closed on their destruction + # automatically. + try: + yield from gen + finally: + if hasattr(gen, "close"): + gen.close() # type: ignore + + # The trick is to start the generator. Then the code execution runs until + # the first dummy None is yielded at which point the context was already + # pushed. This item is discarded. Then when the iteration continues the + # real generator is executed. + wrapped_g = generator() + next(wrapped_g) + return wrapped_g + + +def make_response(*args: t.Any) -> "Response": + """Sometimes it is necessary to set additional headers in a view. Because + views do not have to return response objects but can return a value that + is converted into a response object by Flask itself, it becomes tricky to + add headers to it. This function can be called instead of using a return + and you will get a response object which you can use to attach headers. + + If view looked like this and you want to add a new header:: + + def index(): + return render_template('index.html', foo=42) + + You can now do something like this:: + + def index(): + response = make_response(render_template('index.html', foo=42)) + response.headers['X-Parachutes'] = 'parachutes are cool' + return response + + This function accepts the very same arguments you can return from a + view function. This for example creates a response with a 404 error + code:: + + response = make_response(render_template('not_found.html'), 404) + + The other use case of this function is to force the return value of a + view function into a response which is helpful with view + decorators:: + + response = make_response(view_function()) + response.headers['X-Parachutes'] = 'parachutes are cool' + + Internally this function does the following things: + + - if no arguments are passed, it creates a new response argument + - if one argument is passed, :meth:`flask.Flask.make_response` + is invoked with it. + - if more than one argument is passed, the arguments are passed + to the :meth:`flask.Flask.make_response` function as tuple. + + .. versionadded:: 0.6 + """ + if not args: + return current_app.response_class() + if len(args) == 1: + args = args[0] + return current_app.make_response(args) + + +def url_for(endpoint: str, **values: t.Any) -> str: + """Generates a URL to the given endpoint with the method provided. + + Variable arguments that are unknown to the target endpoint are appended + to the generated URL as query arguments. If the value of a query argument + is ``None``, the whole pair is skipped. In case blueprints are active + you can shortcut references to the same blueprint by prefixing the + local endpoint with a dot (``.``). + + This will reference the index function local to the current blueprint:: + + url_for('.index') + + See :ref:`url-building`. + + Configuration values ``APPLICATION_ROOT`` and ``SERVER_NAME`` are only used when + generating URLs outside of a request context. + + To integrate applications, :class:`Flask` has a hook to intercept URL build + errors through :attr:`Flask.url_build_error_handlers`. The `url_for` + function results in a :exc:`~werkzeug.routing.BuildError` when the current + app does not have a URL for the given endpoint and values. When it does, the + :data:`~flask.current_app` calls its :attr:`~Flask.url_build_error_handlers` if + it is not ``None``, which can return a string to use as the result of + `url_for` (instead of `url_for`'s default to raise the + :exc:`~werkzeug.routing.BuildError` exception) or re-raise the exception. + An example:: + + def external_url_handler(error, endpoint, values): + "Looks up an external URL when `url_for` cannot build a URL." + # This is an example of hooking the build_error_handler. + # Here, lookup_url is some utility function you've built + # which looks up the endpoint in some external URL registry. + url = lookup_url(endpoint, **values) + if url is None: + # External lookup did not have a URL. + # Re-raise the BuildError, in context of original traceback. + exc_type, exc_value, tb = sys.exc_info() + if exc_value is error: + raise exc_type(exc_value).with_traceback(tb) + else: + raise error + # url_for will use this result, instead of raising BuildError. + return url + + app.url_build_error_handlers.append(external_url_handler) + + Here, `error` is the instance of :exc:`~werkzeug.routing.BuildError`, and + `endpoint` and `values` are the arguments passed into `url_for`. Note + that this is for building URLs outside the current application, and not for + handling 404 NotFound errors. + + .. versionadded:: 0.10 + The `_scheme` parameter was added. + + .. versionadded:: 0.9 + The `_anchor` and `_method` parameters were added. + + .. versionadded:: 0.9 + Calls :meth:`Flask.handle_build_error` on + :exc:`~werkzeug.routing.BuildError`. + + :param endpoint: the endpoint of the URL (name of the function) + :param values: the variable arguments of the URL rule + :param _external: if set to ``True``, an absolute URL is generated. Server + address can be changed via ``SERVER_NAME`` configuration variable which + falls back to the `Host` header, then to the IP and port of the request. + :param _scheme: a string specifying the desired URL scheme. The `_external` + parameter must be set to ``True`` or a :exc:`ValueError` is raised. The default + behavior uses the same scheme as the current request, or + :data:`PREFERRED_URL_SCHEME` if no request context is available. + This also can be set to an empty string to build protocol-relative + URLs. + :param _anchor: if provided this is added as anchor to the URL. + :param _method: if provided this explicitly specifies an HTTP method. + """ + appctx = _app_ctx_stack.top + reqctx = _request_ctx_stack.top + + if appctx is None: + raise RuntimeError( + "Attempted to generate a URL without the application context being" + " pushed. This has to be executed when application context is" + " available." + ) + + # If request specific information is available we have some extra + # features that support "relative" URLs. + if reqctx is not None: + url_adapter = reqctx.url_adapter + blueprint_name = request.blueprint + + if endpoint[:1] == ".": + if blueprint_name is not None: + endpoint = f"{blueprint_name}{endpoint}" + else: + endpoint = endpoint[1:] + + external = values.pop("_external", False) + + # Otherwise go with the url adapter from the appctx and make + # the URLs external by default. + else: + url_adapter = appctx.url_adapter + + if url_adapter is None: + raise RuntimeError( + "Application was not able to create a URL adapter for request" + " independent URL generation. You might be able to fix this by" + " setting the SERVER_NAME config variable." + ) + + external = values.pop("_external", True) + + anchor = values.pop("_anchor", None) + method = values.pop("_method", None) + scheme = values.pop("_scheme", None) + appctx.app.inject_url_defaults(endpoint, values) + + # This is not the best way to deal with this but currently the + # underlying Werkzeug router does not support overriding the scheme on + # a per build call basis. + old_scheme = None + if scheme is not None: + if not external: + raise ValueError("When specifying _scheme, _external must be True") + old_scheme = url_adapter.url_scheme + url_adapter.url_scheme = scheme + + try: + try: + rv = url_adapter.build( + endpoint, values, method=method, force_external=external + ) + finally: + if old_scheme is not None: + url_adapter.url_scheme = old_scheme + except BuildError as error: + # We need to inject the values again so that the app callback can + # deal with that sort of stuff. + values["_external"] = external + values["_anchor"] = anchor + values["_method"] = method + values["_scheme"] = scheme + return appctx.app.handle_url_build_error(error, endpoint, values) + + if anchor is not None: + rv += f"#{url_quote(anchor)}" + return rv + + +def get_template_attribute(template_name: str, attribute: str) -> t.Any: + """Loads a macro (or variable) a template exports. This can be used to + invoke a macro from within Python code. If you for example have a + template named :file:`_cider.html` with the following contents: + + .. sourcecode:: html+jinja + + {% macro hello(name) %}Hello {{ name }}!{% endmacro %} + + You can access this from Python code like this:: + + hello = get_template_attribute('_cider.html', 'hello') + return hello('World') + + .. versionadded:: 0.2 + + :param template_name: the name of the template + :param attribute: the name of the variable of macro to access + """ + return getattr(current_app.jinja_env.get_template(template_name).module, attribute) + + +def flash(message: str, category: str = "message") -> None: + """Flashes a message to the next request. In order to remove the + flashed message from the session and to display it to the user, + the template has to call :func:`get_flashed_messages`. + + .. versionchanged:: 0.3 + `category` parameter added. + + :param message: the message to be flashed. + :param category: the category for the message. The following values + are recommended: ``'message'`` for any kind of message, + ``'error'`` for errors, ``'info'`` for information + messages and ``'warning'`` for warnings. However any + kind of string can be used as category. + """ + # Original implementation: + # + # session.setdefault('_flashes', []).append((category, message)) + # + # This assumed that changes made to mutable structures in the session are + # always in sync with the session object, which is not true for session + # implementations that use external storage for keeping their keys/values. + flashes = session.get("_flashes", []) + flashes.append((category, message)) + session["_flashes"] = flashes + message_flashed.send( + current_app._get_current_object(), # type: ignore + message=message, + category=category, + ) + + +def get_flashed_messages( + with_categories: bool = False, category_filter: t.Iterable[str] = () +) -> t.Union[t.List[str], t.List[t.Tuple[str, str]]]: + """Pulls all flashed messages from the session and returns them. + Further calls in the same request to the function will return + the same messages. By default just the messages are returned, + but when `with_categories` is set to ``True``, the return value will + be a list of tuples in the form ``(category, message)`` instead. + + Filter the flashed messages to one or more categories by providing those + categories in `category_filter`. This allows rendering categories in + separate html blocks. The `with_categories` and `category_filter` + arguments are distinct: + + * `with_categories` controls whether categories are returned with message + text (``True`` gives a tuple, where ``False`` gives just the message text). + * `category_filter` filters the messages down to only those matching the + provided categories. + + See :doc:`/patterns/flashing` for examples. + + .. versionchanged:: 0.3 + `with_categories` parameter added. + + .. versionchanged:: 0.9 + `category_filter` parameter added. + + :param with_categories: set to ``True`` to also receive categories. + :param category_filter: filter of categories to limit return values. Only + categories in the list will be returned. + """ + flashes = _request_ctx_stack.top.flashes + if flashes is None: + _request_ctx_stack.top.flashes = flashes = ( + session.pop("_flashes") if "_flashes" in session else [] + ) + if category_filter: + flashes = list(filter(lambda f: f[0] in category_filter, flashes)) + if not with_categories: + return [x[1] for x in flashes] + return flashes + + +def _prepare_send_file_kwargs( + download_name: t.Optional[str] = None, + attachment_filename: t.Optional[str] = None, + etag: t.Optional[t.Union[bool, str]] = None, + add_etags: t.Optional[t.Union[bool]] = None, + max_age: t.Optional[ + t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]] + ] = None, + cache_timeout: t.Optional[int] = None, + **kwargs: t.Any, +) -> t.Dict[str, t.Any]: + if attachment_filename is not None: + warnings.warn( + "The 'attachment_filename' parameter has been renamed to" + " 'download_name'. The old name will be removed in Flask" + " 2.1.", + DeprecationWarning, + stacklevel=3, + ) + download_name = attachment_filename + + if cache_timeout is not None: + warnings.warn( + "The 'cache_timeout' parameter has been renamed to" + " 'max_age'. The old name will be removed in Flask 2.1.", + DeprecationWarning, + stacklevel=3, + ) + max_age = cache_timeout + + if add_etags is not None: + warnings.warn( + "The 'add_etags' parameter has been renamed to 'etag'. The" + " old name will be removed in Flask 2.1.", + DeprecationWarning, + stacklevel=3, + ) + etag = add_etags + + if max_age is None: + max_age = current_app.get_send_file_max_age + + kwargs.update( + environ=request.environ, + download_name=download_name, + etag=etag, + max_age=max_age, + use_x_sendfile=current_app.use_x_sendfile, + response_class=current_app.response_class, + _root_path=current_app.root_path, # type: ignore + ) + return kwargs + + +def send_file( + path_or_file: t.Union[os.PathLike, str, t.BinaryIO], + mimetype: t.Optional[str] = None, + as_attachment: bool = False, + download_name: t.Optional[str] = None, + attachment_filename: t.Optional[str] = None, + conditional: bool = True, + etag: t.Union[bool, str] = True, + add_etags: t.Optional[bool] = None, + last_modified: t.Optional[t.Union[datetime, int, float]] = None, + max_age: t.Optional[ + t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]] + ] = None, + cache_timeout: t.Optional[int] = None, +): + """Send the contents of a file to the client. + + The first argument can be a file path or a file-like object. Paths + are preferred in most cases because Werkzeug can manage the file and + get extra information from the path. Passing a file-like object + requires that the file is opened in binary mode, and is mostly + useful when building a file in memory with :class:`io.BytesIO`. + + Never pass file paths provided by a user. The path is assumed to be + trusted, so a user could craft a path to access a file you didn't + intend. Use :func:`send_from_directory` to safely serve + user-requested paths from within a directory. + + If the WSGI server sets a ``file_wrapper`` in ``environ``, it is + used, otherwise Werkzeug's built-in wrapper is used. Alternatively, + if the HTTP server supports ``X-Sendfile``, configuring Flask with + ``USE_X_SENDFILE = True`` will tell the server to send the given + path, which is much more efficient than reading it in Python. + + :param path_or_file: The path to the file to send, relative to the + current working directory if a relative path is given. + Alternatively, a file-like object opened in binary mode. Make + sure the file pointer is seeked to the start of the data. + :param mimetype: The MIME type to send for the file. If not + provided, it will try to detect it from the file name. + :param as_attachment: Indicate to a browser that it should offer to + save the file instead of displaying it. + :param download_name: The default name browsers will use when saving + the file. Defaults to the passed file name. + :param conditional: Enable conditional and range responses based on + request headers. Requires passing a file path and ``environ``. + :param etag: Calculate an ETag for the file, which requires passing + a file path. Can also be a string to use instead. + :param last_modified: The last modified time to send for the file, + in seconds. If not provided, it will try to detect it from the + file path. + :param max_age: How long the client should cache the file, in + seconds. If set, ``Cache-Control`` will be ``public``, otherwise + it will be ``no-cache`` to prefer conditional caching. + + .. versionchanged:: 2.0 + ``download_name`` replaces the ``attachment_filename`` + parameter. If ``as_attachment=False``, it is passed with + ``Content-Disposition: inline`` instead. + + .. versionchanged:: 2.0 + ``max_age`` replaces the ``cache_timeout`` parameter. + ``conditional`` is enabled and ``max_age`` is not set by + default. + + .. versionchanged:: 2.0 + ``etag`` replaces the ``add_etags`` parameter. It can be a + string to use instead of generating one. + + .. versionchanged:: 2.0 + Passing a file-like object that inherits from + :class:`~io.TextIOBase` will raise a :exc:`ValueError` rather + than sending an empty file. + + .. versionadded:: 2.0 + Moved the implementation to Werkzeug. This is now a wrapper to + pass some Flask-specific arguments. + + .. versionchanged:: 1.1 + ``filename`` may be a :class:`~os.PathLike` object. + + .. versionchanged:: 1.1 + Passing a :class:`~io.BytesIO` object supports range requests. + + .. versionchanged:: 1.0.3 + Filenames are encoded with ASCII instead of Latin-1 for broader + compatibility with WSGI servers. + + .. versionchanged:: 1.0 + UTF-8 filenames as specified in :rfc:`2231` are supported. + + .. versionchanged:: 0.12 + The filename is no longer automatically inferred from file + objects. If you want to use automatic MIME and etag support, + pass a filename via ``filename_or_fp`` or + ``attachment_filename``. + + .. versionchanged:: 0.12 + ``attachment_filename`` is preferred over ``filename`` for MIME + detection. + + .. versionchanged:: 0.9 + ``cache_timeout`` defaults to + :meth:`Flask.get_send_file_max_age`. + + .. versionchanged:: 0.7 + MIME guessing and etag support for file-like objects was + deprecated because it was unreliable. Pass a filename if you are + able to, otherwise attach an etag yourself. + + .. versionchanged:: 0.5 + The ``add_etags``, ``cache_timeout`` and ``conditional`` + parameters were added. The default behavior is to add etags. + + .. versionadded:: 0.2 + """ + return werkzeug.utils.send_file( + **_prepare_send_file_kwargs( + path_or_file=path_or_file, + environ=request.environ, + mimetype=mimetype, + as_attachment=as_attachment, + download_name=download_name, + attachment_filename=attachment_filename, + conditional=conditional, + etag=etag, + add_etags=add_etags, + last_modified=last_modified, + max_age=max_age, + cache_timeout=cache_timeout, + ) + ) + + +def safe_join(directory: str, *pathnames: str) -> str: + """Safely join zero or more untrusted path components to a base + directory to avoid escaping the base directory. + + :param directory: The trusted base directory. + :param pathnames: The untrusted path components relative to the + base directory. + :return: A safe path, otherwise ``None``. + """ + warnings.warn( + "'flask.helpers.safe_join' is deprecated and will be removed in" + " Flask 2.1. Use 'werkzeug.utils.safe_join' instead.", + DeprecationWarning, + stacklevel=2, + ) + path = werkzeug.utils.safe_join(directory, *pathnames) + + if path is None: + raise NotFound() + + return path + + +def send_from_directory( + directory: t.Union[os.PathLike, str], + path: t.Union[os.PathLike, str], + filename: t.Optional[str] = None, + **kwargs: t.Any, +) -> "Response": + """Send a file from within a directory using :func:`send_file`. + + .. code-block:: python + + @app.route("/uploads/") + def download_file(name): + return send_from_directory( + app.config['UPLOAD_FOLDER'], name, as_attachment=True + ) + + This is a secure way to serve files from a folder, such as static + files or uploads. Uses :func:`~werkzeug.security.safe_join` to + ensure the path coming from the client is not maliciously crafted to + point outside the specified directory. + + If the final path does not point to an existing regular file, + raises a 404 :exc:`~werkzeug.exceptions.NotFound` error. + + :param directory: The directory that ``path`` must be located under. + :param path: The path to the file to send, relative to + ``directory``. + :param kwargs: Arguments to pass to :func:`send_file`. + + .. versionchanged:: 2.0 + ``path`` replaces the ``filename`` parameter. + + .. versionadded:: 2.0 + Moved the implementation to Werkzeug. This is now a wrapper to + pass some Flask-specific arguments. + + .. versionadded:: 0.5 + """ + if filename is not None: + warnings.warn( + "The 'filename' parameter has been renamed to 'path'. The" + " old name will be removed in Flask 2.1.", + DeprecationWarning, + stacklevel=2, + ) + path = filename + + return werkzeug.utils.send_from_directory( # type: ignore + directory, path, **_prepare_send_file_kwargs(**kwargs) + ) + + +def get_root_path(import_name: str) -> str: + """Find the root path of a package, or the path that contains a + module. If it cannot be found, returns the current working + directory. + + Not to be confused with the value returned by :func:`find_package`. + + :meta private: + """ + # Module already imported and has a file attribute. Use that first. + mod = sys.modules.get(import_name) + + if mod is not None and hasattr(mod, "__file__"): + return os.path.dirname(os.path.abspath(mod.__file__)) + + # Next attempt: check the loader. + loader = pkgutil.get_loader(import_name) + + # Loader does not exist or we're referring to an unloaded main + # module or a main module without path (interactive sessions), go + # with the current working directory. + if loader is None or import_name == "__main__": + return os.getcwd() + + if hasattr(loader, "get_filename"): + filepath = loader.get_filename(import_name) # type: ignore + else: + # Fall back to imports. + __import__(import_name) + mod = sys.modules[import_name] + filepath = getattr(mod, "__file__", None) + + # If we don't have a file path it might be because it is a + # namespace package. In this case pick the root path from the + # first module that is contained in the package. + if filepath is None: + raise RuntimeError( + "No root path can be found for the provided module" + f" {import_name!r}. This can happen because the module" + " came from an import hook that does not provide file" + " name information or because it's a namespace package." + " In this case the root path needs to be explicitly" + " provided." + ) + + # filepath is import_name.py for a module, or __init__.py for a package. + return os.path.dirname(os.path.abspath(filepath)) + + +class locked_cached_property(werkzeug.utils.cached_property): + """A :func:`property` that is only evaluated once. Like + :class:`werkzeug.utils.cached_property` except access uses a lock + for thread safety. + + .. versionchanged:: 2.0 + Inherits from Werkzeug's ``cached_property`` (and ``property``). + """ + + def __init__( + self, + fget: t.Callable[[t.Any], t.Any], + name: t.Optional[str] = None, + doc: t.Optional[str] = None, + ) -> None: + super().__init__(fget, name=name, doc=doc) + self.lock = RLock() + + def __get__(self, obj: object, type: type = None) -> t.Any: # type: ignore + if obj is None: + return self + + with self.lock: + return super().__get__(obj, type=type) + + def __set__(self, obj: object, value: t.Any) -> None: + with self.lock: + super().__set__(obj, value) + + def __delete__(self, obj: object) -> None: + with self.lock: + super().__delete__(obj) + + +def total_seconds(td: timedelta) -> int: + """Returns the total seconds from a timedelta object. + + :param timedelta td: the timedelta to be converted in seconds + + :returns: number of seconds + :rtype: int + + .. deprecated:: 2.0 + Will be removed in Flask 2.1. Use + :meth:`timedelta.total_seconds` instead. + """ + warnings.warn( + "'total_seconds' is deprecated and will be removed in Flask" + " 2.1. Use 'timedelta.total_seconds' instead.", + DeprecationWarning, + stacklevel=2, + ) + return td.days * 60 * 60 * 24 + td.seconds + + +def is_ip(value: str) -> bool: + """Determine if the given string is an IP address. + + :param value: value to check + :type value: str + + :return: True if string is an IP address + :rtype: bool + """ + for family in (socket.AF_INET, socket.AF_INET6): + try: + socket.inet_pton(family, value) + except OSError: + pass + else: + return True + + return False + + +@lru_cache(maxsize=None) +def _split_blueprint_path(name: str) -> t.List[str]: + out: t.List[str] = [name] + + if "." in name: + out.extend(_split_blueprint_path(name.rpartition(".")[0])) + + return out diff --git a/flask-app/venv/lib/python3.8/site-packages/flask/json/__init__.py b/flask-app/venv/lib/python3.8/site-packages/flask/json/__init__.py new file mode 100644 index 000000000..10d5123a3 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/flask/json/__init__.py @@ -0,0 +1,357 @@ +import decimal +import io +import json as _json +import typing as t +import uuid +import warnings +from datetime import date + +from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps +from werkzeug.http import http_date + +from ..globals import current_app +from ..globals import request + +if t.TYPE_CHECKING: + from ..app import Flask + from ..wrappers import Response + +try: + import dataclasses +except ImportError: + # Python < 3.7 + dataclasses = None # type: ignore + + +class JSONEncoder(_json.JSONEncoder): + """The default JSON encoder. Handles extra types compared to the + built-in :class:`json.JSONEncoder`. + + - :class:`datetime.datetime` and :class:`datetime.date` are + serialized to :rfc:`822` strings. This is the same as the HTTP + date format. + - :class:`uuid.UUID` is serialized to a string. + - :class:`dataclasses.dataclass` is passed to + :func:`dataclasses.asdict`. + - :class:`~markupsafe.Markup` (or any object with a ``__html__`` + method) will call the ``__html__`` method to get a string. + + Assign a subclass of this to :attr:`flask.Flask.json_encoder` or + :attr:`flask.Blueprint.json_encoder` to override the default. + """ + + def default(self, o: t.Any) -> t.Any: + """Convert ``o`` to a JSON serializable type. See + :meth:`json.JSONEncoder.default`. Python does not support + overriding how basic types like ``str`` or ``list`` are + serialized, they are handled before this method. + """ + if isinstance(o, date): + return http_date(o) + if isinstance(o, (decimal.Decimal, uuid.UUID)): + return str(o) + if dataclasses and dataclasses.is_dataclass(o): + return dataclasses.asdict(o) + if hasattr(o, "__html__"): + return str(o.__html__()) + return super().default(o) + + +class JSONDecoder(_json.JSONDecoder): + """The default JSON decoder. + + This does not change any behavior from the built-in + :class:`json.JSONDecoder`. + + Assign a subclass of this to :attr:`flask.Flask.json_decoder` or + :attr:`flask.Blueprint.json_decoder` to override the default. + """ + + +def _dump_arg_defaults( + kwargs: t.Dict[str, t.Any], app: t.Optional["Flask"] = None +) -> None: + """Inject default arguments for dump functions.""" + if app is None: + app = current_app + + if app: + cls = app.json_encoder + bp = app.blueprints.get(request.blueprint) if request else None # type: ignore + if bp is not None and bp.json_encoder is not None: + cls = bp.json_encoder + + kwargs.setdefault("cls", cls) + kwargs.setdefault("ensure_ascii", app.config["JSON_AS_ASCII"]) + kwargs.setdefault("sort_keys", app.config["JSON_SORT_KEYS"]) + else: + kwargs.setdefault("sort_keys", True) + kwargs.setdefault("cls", JSONEncoder) + + +def _load_arg_defaults( + kwargs: t.Dict[str, t.Any], app: t.Optional["Flask"] = None +) -> None: + """Inject default arguments for load functions.""" + if app is None: + app = current_app + + if app: + cls = app.json_decoder + bp = app.blueprints.get(request.blueprint) if request else None # type: ignore + if bp is not None and bp.json_decoder is not None: + cls = bp.json_decoder + + kwargs.setdefault("cls", cls) + else: + kwargs.setdefault("cls", JSONDecoder) + + +def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str: + """Serialize an object to a string of JSON. + + Takes the same arguments as the built-in :func:`json.dumps`, with + some defaults from application configuration. + + :param obj: Object to serialize to JSON. + :param app: Use this app's config instead of the active app context + or defaults. + :param kwargs: Extra arguments passed to :func:`json.dumps`. + + .. versionchanged:: 2.0.2 + :class:`decimal.Decimal` is supported by converting to a string. + + .. versionchanged:: 2.0 + ``encoding`` is deprecated and will be removed in Flask 2.1. + + .. versionchanged:: 1.0.3 + ``app`` can be passed directly, rather than requiring an app + context for configuration. + """ + _dump_arg_defaults(kwargs, app=app) + encoding = kwargs.pop("encoding", None) + rv = _json.dumps(obj, **kwargs) + + if encoding is not None: + warnings.warn( + "'encoding' is deprecated and will be removed in Flask 2.1.", + DeprecationWarning, + stacklevel=2, + ) + + if isinstance(rv, str): + return rv.encode(encoding) # type: ignore + + return rv + + +def dump( + obj: t.Any, fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any +) -> None: + """Serialize an object to JSON written to a file object. + + Takes the same arguments as the built-in :func:`json.dump`, with + some defaults from application configuration. + + :param obj: Object to serialize to JSON. + :param fp: File object to write JSON to. + :param app: Use this app's config instead of the active app context + or defaults. + :param kwargs: Extra arguments passed to :func:`json.dump`. + + .. versionchanged:: 2.0 + Writing to a binary file, and the ``encoding`` argument, is + deprecated and will be removed in Flask 2.1. + """ + _dump_arg_defaults(kwargs, app=app) + encoding = kwargs.pop("encoding", None) + show_warning = encoding is not None + + try: + fp.write("") + except TypeError: + show_warning = True + fp = io.TextIOWrapper(fp, encoding or "utf-8") # type: ignore + + if show_warning: + warnings.warn( + "Writing to a binary file, and the 'encoding' argument, is" + " deprecated and will be removed in Flask 2.1.", + DeprecationWarning, + stacklevel=2, + ) + + _json.dump(obj, fp, **kwargs) + + +def loads(s: str, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.Any: + """Deserialize an object from a string of JSON. + + Takes the same arguments as the built-in :func:`json.loads`, with + some defaults from application configuration. + + :param s: JSON string to deserialize. + :param app: Use this app's config instead of the active app context + or defaults. + :param kwargs: Extra arguments passed to :func:`json.loads`. + + .. versionchanged:: 2.0 + ``encoding`` is deprecated and will be removed in Flask 2.1. The + data must be a string or UTF-8 bytes. + + .. versionchanged:: 1.0.3 + ``app`` can be passed directly, rather than requiring an app + context for configuration. + """ + _load_arg_defaults(kwargs, app=app) + encoding = kwargs.pop("encoding", None) + + if encoding is not None: + warnings.warn( + "'encoding' is deprecated and will be removed in Flask 2.1." + " The data must be a string or UTF-8 bytes.", + DeprecationWarning, + stacklevel=2, + ) + + if isinstance(s, bytes): + s = s.decode(encoding) + + return _json.loads(s, **kwargs) + + +def load(fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.Any: + """Deserialize an object from JSON read from a file object. + + Takes the same arguments as the built-in :func:`json.load`, with + some defaults from application configuration. + + :param fp: File object to read JSON from. + :param app: Use this app's config instead of the active app context + or defaults. + :param kwargs: Extra arguments passed to :func:`json.load`. + + .. versionchanged:: 2.0 + ``encoding`` is deprecated and will be removed in Flask 2.1. The + file must be text mode, or binary mode with UTF-8 bytes. + """ + _load_arg_defaults(kwargs, app=app) + encoding = kwargs.pop("encoding", None) + + if encoding is not None: + warnings.warn( + "'encoding' is deprecated and will be removed in Flask 2.1." + " The file must be text mode, or binary mode with UTF-8" + " bytes.", + DeprecationWarning, + stacklevel=2, + ) + + if isinstance(fp.read(0), bytes): + fp = io.TextIOWrapper(fp, encoding) # type: ignore + + return _json.load(fp, **kwargs) + + +def htmlsafe_dumps(obj: t.Any, **kwargs: t.Any) -> str: + """Serialize an object to a string of JSON with :func:`dumps`, then + replace HTML-unsafe characters with Unicode escapes and mark the + result safe with :class:`~markupsafe.Markup`. + + This is available in templates as the ``|tojson`` filter. + + The returned string is safe to render in HTML documents and + ``') + # => <script> do_nasty_stuff() </script> + # sanitize_html('Click here for $100') + # => Click here for $100 + def sanitize_token(self, token): + + # accommodate filters which use token_type differently + token_type = token["type"] + if token_type in ("StartTag", "EndTag", "EmptyTag"): + name = token["name"] + namespace = token["namespace"] + if ((namespace, name) in self.allowed_elements or + (namespace is None and + (namespaces["html"], name) in self.allowed_elements)): + return self.allowed_token(token) + else: + return self.disallowed_token(token) + elif token_type == "Comment": + pass + else: + return token + + def allowed_token(self, token): + if "data" in token: + attrs = token["data"] + attr_names = set(attrs.keys()) + + # Remove forbidden attributes + for to_remove in (attr_names - self.allowed_attributes): + del token["data"][to_remove] + attr_names.remove(to_remove) + + # Remove attributes with disallowed URL values + for attr in (attr_names & self.attr_val_is_uri): + assert attr in attrs + # I don't have a clue where this regexp comes from or why it matches those + # characters, nor why we call unescape. I just know it's always been here. + # Should you be worried by this comment in a sanitizer? Yes. On the other hand, all + # this will do is remove *more* than it otherwise would. + val_unescaped = re.sub("[`\x00-\x20\x7f-\xa0\\s]+", '', + unescape(attrs[attr])).lower() + # remove replacement characters from unescaped characters + val_unescaped = val_unescaped.replace("\ufffd", "") + try: + uri = urlparse.urlparse(val_unescaped) + except ValueError: + uri = None + del attrs[attr] + if uri and uri.scheme: + if uri.scheme not in self.allowed_protocols: + del attrs[attr] + if uri.scheme == 'data': + m = data_content_type.match(uri.path) + if not m: + del attrs[attr] + elif m.group('content_type') not in self.allowed_content_types: + del attrs[attr] + + for attr in self.svg_attr_val_allows_ref: + if attr in attrs: + attrs[attr] = re.sub(r'url\s*\(\s*[^#\s][^)]+?\)', + ' ', + unescape(attrs[attr])) + if (token["name"] in self.svg_allow_local_href and + (namespaces['xlink'], 'href') in attrs and re.search(r'^\s*[^#\s].*', + attrs[(namespaces['xlink'], 'href')])): + del attrs[(namespaces['xlink'], 'href')] + if (None, 'style') in attrs: + attrs[(None, 'style')] = self.sanitize_css(attrs[(None, 'style')]) + token["data"] = attrs + return token + + def disallowed_token(self, token): + token_type = token["type"] + if token_type == "EndTag": + token["data"] = "" % token["name"] + elif token["data"]: + assert token_type in ("StartTag", "EmptyTag") + attrs = [] + for (ns, name), v in token["data"].items(): + attrs.append(' %s="%s"' % (name if ns is None else "%s:%s" % (prefixes[ns], name), escape(v))) + token["data"] = "<%s%s>" % (token["name"], ''.join(attrs)) + else: + token["data"] = "<%s>" % token["name"] + if token.get("selfClosing"): + token["data"] = token["data"][:-1] + "/>" + + token["type"] = "Characters" + + del token["name"] + return token + + def sanitize_css(self, style): + # disallow urls + style = re.compile(r'url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ', style) + + # gauntlet + if not re.match(r"""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): + return '' + if not re.match(r"^\s*([-\w]+\s*:[^:;]*(;\s*|$))*$", style): + return '' + + clean = [] + for prop, value in re.findall(r"([-\w]+)\s*:\s*([^:;]*)", style): + if not value: + continue + if prop.lower() in self.allowed_css_properties: + clean.append(prop + ': ' + value + ';') + elif prop.split('-')[0].lower() in ['background', 'border', 'margin', + 'padding']: + for keyword in value.split(): + if keyword not in self.allowed_css_keywords and \ + not re.match(r"^(#[0-9a-fA-F]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$", keyword): # noqa + break + else: + clean.append(prop + ': ' + value + ';') + elif prop.lower() in self.allowed_svg_properties: + clean.append(prop + ': ' + value + ';') + + return ' '.join(clean) diff --git a/flask-app/venv/lib/python3.8/site-packages/pip/_vendor/html5lib/filters/whitespace.py b/flask-app/venv/lib/python3.8/site-packages/pip/_vendor/html5lib/filters/whitespace.py new file mode 100644 index 000000000..0d12584b4 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/pip/_vendor/html5lib/filters/whitespace.py @@ -0,0 +1,38 @@ +from __future__ import absolute_import, division, unicode_literals + +import re + +from . import base +from ..constants import rcdataElements, spaceCharacters +spaceCharacters = "".join(spaceCharacters) + +SPACES_REGEX = re.compile("[%s]+" % spaceCharacters) + + +class Filter(base.Filter): + """Collapses whitespace except in pre, textarea, and script elements""" + spacePreserveElements = frozenset(["pre", "textarea"] + list(rcdataElements)) + + def __iter__(self): + preserve = 0 + for token in base.Filter.__iter__(self): + type = token["type"] + if type == "StartTag" \ + and (preserve or token["name"] in self.spacePreserveElements): + preserve += 1 + + elif type == "EndTag" and preserve: + preserve -= 1 + + elif not preserve and type == "SpaceCharacters" and token["data"]: + # Test on token["data"] above to not introduce spaces where there were not + token["data"] = " " + + elif not preserve and type == "Characters": + token["data"] = collapse_spaces(token["data"]) + + yield token + + +def collapse_spaces(text): + return SPACES_REGEX.sub(' ', text) diff --git a/flask-app/venv/lib/python3.8/site-packages/pip/_vendor/html5lib/html5parser.py b/flask-app/venv/lib/python3.8/site-packages/pip/_vendor/html5lib/html5parser.py new file mode 100644 index 000000000..ae41a1337 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/pip/_vendor/html5lib/html5parser.py @@ -0,0 +1,2791 @@ +from __future__ import absolute_import, division, unicode_literals +from pip._vendor.six import with_metaclass, viewkeys + +import types +from collections import OrderedDict + +from . import _inputstream +from . import _tokenizer + +from . import treebuilders +from .treebuilders.base import Marker + +from . import _utils +from .constants import ( + spaceCharacters, asciiUpper2Lower, + specialElements, headingElements, cdataElements, rcdataElements, + tokenTypes, tagTokenTypes, + namespaces, + htmlIntegrationPointElements, mathmlTextIntegrationPointElements, + adjustForeignAttributes as adjustForeignAttributesMap, + adjustMathMLAttributes, adjustSVGAttributes, + E, + _ReparseException +) + + +def parse(doc, treebuilder="etree", namespaceHTMLElements=True, **kwargs): + """Parse an HTML document as a string or file-like object into a tree + + :arg doc: the document to parse as a string or file-like object + + :arg treebuilder: the treebuilder to use when parsing + + :arg namespaceHTMLElements: whether or not to namespace HTML elements + + :returns: parsed tree + + Example: + + >>> from html5lib.html5parser import parse + >>> parse('

This is a doc

') + + + """ + tb = treebuilders.getTreeBuilder(treebuilder) + p = HTMLParser(tb, namespaceHTMLElements=namespaceHTMLElements) + return p.parse(doc, **kwargs) + + +def parseFragment(doc, container="div", treebuilder="etree", namespaceHTMLElements=True, **kwargs): + """Parse an HTML fragment as a string or file-like object into a tree + + :arg doc: the fragment to parse as a string or file-like object + + :arg container: the container context to parse the fragment in + + :arg treebuilder: the treebuilder to use when parsing + + :arg namespaceHTMLElements: whether or not to namespace HTML elements + + :returns: parsed tree + + Example: + + >>> from html5lib.html5libparser import parseFragment + >>> parseFragment('this is a fragment') + + + """ + tb = treebuilders.getTreeBuilder(treebuilder) + p = HTMLParser(tb, namespaceHTMLElements=namespaceHTMLElements) + return p.parseFragment(doc, container=container, **kwargs) + + +def method_decorator_metaclass(function): + class Decorated(type): + def __new__(meta, classname, bases, classDict): + for attributeName, attribute in classDict.items(): + if isinstance(attribute, types.FunctionType): + attribute = function(attribute) + + classDict[attributeName] = attribute + return type.__new__(meta, classname, bases, classDict) + return Decorated + + +class HTMLParser(object): + """HTML parser + + Generates a tree structure from a stream of (possibly malformed) HTML. + + """ + + def __init__(self, tree=None, strict=False, namespaceHTMLElements=True, debug=False): + """ + :arg tree: a treebuilder class controlling the type of tree that will be + returned. Built in treebuilders can be accessed through + html5lib.treebuilders.getTreeBuilder(treeType) + + :arg strict: raise an exception when a parse error is encountered + + :arg namespaceHTMLElements: whether or not to namespace HTML elements + + :arg debug: whether or not to enable debug mode which logs things + + Example: + + >>> from html5lib.html5parser import HTMLParser + >>> parser = HTMLParser() # generates parser with etree builder + >>> parser = HTMLParser('lxml', strict=True) # generates parser with lxml builder which is strict + + """ + + # Raise an exception on the first error encountered + self.strict = strict + + if tree is None: + tree = treebuilders.getTreeBuilder("etree") + self.tree = tree(namespaceHTMLElements) + self.errors = [] + + self.phases = dict([(name, cls(self, self.tree)) for name, cls in + getPhases(debug).items()]) + + def _parse(self, stream, innerHTML=False, container="div", scripting=False, **kwargs): + + self.innerHTMLMode = innerHTML + self.container = container + self.scripting = scripting + self.tokenizer = _tokenizer.HTMLTokenizer(stream, parser=self, **kwargs) + self.reset() + + try: + self.mainLoop() + except _ReparseException: + self.reset() + self.mainLoop() + + def reset(self): + self.tree.reset() + self.firstStartTag = False + self.errors = [] + self.log = [] # only used with debug mode + # "quirks" / "limited quirks" / "no quirks" + self.compatMode = "no quirks" + + if self.innerHTMLMode: + self.innerHTML = self.container.lower() + + if self.innerHTML in cdataElements: + self.tokenizer.state = self.tokenizer.rcdataState + elif self.innerHTML in rcdataElements: + self.tokenizer.state = self.tokenizer.rawtextState + elif self.innerHTML == 'plaintext': + self.tokenizer.state = self.tokenizer.plaintextState + else: + # state already is data state + # self.tokenizer.state = self.tokenizer.dataState + pass + self.phase = self.phases["beforeHtml"] + self.phase.insertHtmlElement() + self.resetInsertionMode() + else: + self.innerHTML = False # pylint:disable=redefined-variable-type + self.phase = self.phases["initial"] + + self.lastPhase = None + + self.beforeRCDataPhase = None + + self.framesetOK = True + + @property + def documentEncoding(self): + """Name of the character encoding that was used to decode the input stream, or + :obj:`None` if that is not determined yet + + """ + if not hasattr(self, 'tokenizer'): + return None + return self.tokenizer.stream.charEncoding[0].name + + def isHTMLIntegrationPoint(self, element): + if (element.name == "annotation-xml" and + element.namespace == namespaces["mathml"]): + return ("encoding" in element.attributes and + element.attributes["encoding"].translate( + asciiUpper2Lower) in + ("text/html", "application/xhtml+xml")) + else: + return (element.namespace, element.name) in htmlIntegrationPointElements + + def isMathMLTextIntegrationPoint(self, element): + return (element.namespace, element.name) in mathmlTextIntegrationPointElements + + def mainLoop(self): + CharactersToken = tokenTypes["Characters"] + SpaceCharactersToken = tokenTypes["SpaceCharacters"] + StartTagToken = tokenTypes["StartTag"] + EndTagToken = tokenTypes["EndTag"] + CommentToken = tokenTypes["Comment"] + DoctypeToken = tokenTypes["Doctype"] + ParseErrorToken = tokenTypes["ParseError"] + + for token in self.normalizedTokens(): + prev_token = None + new_token = token + while new_token is not None: + prev_token = new_token + currentNode = self.tree.openElements[-1] if self.tree.openElements else None + currentNodeNamespace = currentNode.namespace if currentNode else None + currentNodeName = currentNode.name if currentNode else None + + type = new_token["type"] + + if type == ParseErrorToken: + self.parseError(new_token["data"], new_token.get("datavars", {})) + new_token = None + else: + if (len(self.tree.openElements) == 0 or + currentNodeNamespace == self.tree.defaultNamespace or + (self.isMathMLTextIntegrationPoint(currentNode) and + ((type == StartTagToken and + token["name"] not in frozenset(["mglyph", "malignmark"])) or + type in (CharactersToken, SpaceCharactersToken))) or + (currentNodeNamespace == namespaces["mathml"] and + currentNodeName == "annotation-xml" and + type == StartTagToken and + token["name"] == "svg") or + (self.isHTMLIntegrationPoint(currentNode) and + type in (StartTagToken, CharactersToken, SpaceCharactersToken))): + phase = self.phase + else: + phase = self.phases["inForeignContent"] + + if type == CharactersToken: + new_token = phase.processCharacters(new_token) + elif type == SpaceCharactersToken: + new_token = phase.processSpaceCharacters(new_token) + elif type == StartTagToken: + new_token = phase.processStartTag(new_token) + elif type == EndTagToken: + new_token = phase.processEndTag(new_token) + elif type == CommentToken: + new_token = phase.processComment(new_token) + elif type == DoctypeToken: + new_token = phase.processDoctype(new_token) + + if (type == StartTagToken and prev_token["selfClosing"] and + not prev_token["selfClosingAcknowledged"]): + self.parseError("non-void-element-with-trailing-solidus", + {"name": prev_token["name"]}) + + # When the loop finishes it's EOF + reprocess = True + phases = [] + while reprocess: + phases.append(self.phase) + reprocess = self.phase.processEOF() + if reprocess: + assert self.phase not in phases + + def normalizedTokens(self): + for token in self.tokenizer: + yield self.normalizeToken(token) + + def parse(self, stream, *args, **kwargs): + """Parse a HTML document into a well-formed tree + + :arg stream: a file-like object or string containing the HTML to be parsed + + The optional encoding parameter must be a string that indicates + the encoding. If specified, that encoding will be used, + regardless of any BOM or later declaration (such as in a meta + element). + + :arg scripting: treat noscript elements as if JavaScript was turned on + + :returns: parsed tree + + Example: + + >>> from html5lib.html5parser import HTMLParser + >>> parser = HTMLParser() + >>> parser.parse('

This is a doc

') + + + """ + self._parse(stream, False, None, *args, **kwargs) + return self.tree.getDocument() + + def parseFragment(self, stream, *args, **kwargs): + """Parse a HTML fragment into a well-formed tree fragment + + :arg container: name of the element we're setting the innerHTML + property if set to None, default to 'div' + + :arg stream: a file-like object or string containing the HTML to be parsed + + The optional encoding parameter must be a string that indicates + the encoding. If specified, that encoding will be used, + regardless of any BOM or later declaration (such as in a meta + element) + + :arg scripting: treat noscript elements as if JavaScript was turned on + + :returns: parsed tree + + Example: + + >>> from html5lib.html5libparser import HTMLParser + >>> parser = HTMLParser() + >>> parser.parseFragment('this is a fragment') + + + """ + self._parse(stream, True, *args, **kwargs) + return self.tree.getFragment() + + def parseError(self, errorcode="XXX-undefined-error", datavars=None): + # XXX The idea is to make errorcode mandatory. + if datavars is None: + datavars = {} + self.errors.append((self.tokenizer.stream.position(), errorcode, datavars)) + if self.strict: + raise ParseError(E[errorcode] % datavars) + + def normalizeToken(self, token): + # HTML5 specific normalizations to the token stream + if token["type"] == tokenTypes["StartTag"]: + raw = token["data"] + token["data"] = OrderedDict(raw) + if len(raw) > len(token["data"]): + # we had some duplicated attribute, fix so first wins + token["data"].update(raw[::-1]) + + return token + + def adjustMathMLAttributes(self, token): + adjust_attributes(token, adjustMathMLAttributes) + + def adjustSVGAttributes(self, token): + adjust_attributes(token, adjustSVGAttributes) + + def adjustForeignAttributes(self, token): + adjust_attributes(token, adjustForeignAttributesMap) + + def reparseTokenNormal(self, token): + # pylint:disable=unused-argument + self.parser.phase() + + def resetInsertionMode(self): + # The name of this method is mostly historical. (It's also used in the + # specification.) + last = False + newModes = { + "select": "inSelect", + "td": "inCell", + "th": "inCell", + "tr": "inRow", + "tbody": "inTableBody", + "thead": "inTableBody", + "tfoot": "inTableBody", + "caption": "inCaption", + "colgroup": "inColumnGroup", + "table": "inTable", + "head": "inBody", + "body": "inBody", + "frameset": "inFrameset", + "html": "beforeHead" + } + for node in self.tree.openElements[::-1]: + nodeName = node.name + new_phase = None + if node == self.tree.openElements[0]: + assert self.innerHTML + last = True + nodeName = self.innerHTML + # Check for conditions that should only happen in the innerHTML + # case + if nodeName in ("select", "colgroup", "head", "html"): + assert self.innerHTML + + if not last and node.namespace != self.tree.defaultNamespace: + continue + + if nodeName in newModes: + new_phase = self.phases[newModes[nodeName]] + break + elif last: + new_phase = self.phases["inBody"] + break + + self.phase = new_phase + + def parseRCDataRawtext(self, token, contentType): + # Generic RCDATA/RAWTEXT Parsing algorithm + assert contentType in ("RAWTEXT", "RCDATA") + + self.tree.insertElement(token) + + if contentType == "RAWTEXT": + self.tokenizer.state = self.tokenizer.rawtextState + else: + self.tokenizer.state = self.tokenizer.rcdataState + + self.originalPhase = self.phase + + self.phase = self.phases["text"] + + +@_utils.memoize +def getPhases(debug): + def log(function): + """Logger that records which phase processes each token""" + type_names = dict((value, key) for key, value in + tokenTypes.items()) + + def wrapped(self, *args, **kwargs): + if function.__name__.startswith("process") and len(args) > 0: + token = args[0] + try: + info = {"type": type_names[token['type']]} + except: + raise + if token['type'] in tagTokenTypes: + info["name"] = token['name'] + + self.parser.log.append((self.parser.tokenizer.state.__name__, + self.parser.phase.__class__.__name__, + self.__class__.__name__, + function.__name__, + info)) + return function(self, *args, **kwargs) + else: + return function(self, *args, **kwargs) + return wrapped + + def getMetaclass(use_metaclass, metaclass_func): + if use_metaclass: + return method_decorator_metaclass(metaclass_func) + else: + return type + + # pylint:disable=unused-argument + class Phase(with_metaclass(getMetaclass(debug, log))): + """Base class for helper object that implements each phase of processing + """ + + def __init__(self, parser, tree): + self.parser = parser + self.tree = tree + + def processEOF(self): + raise NotImplementedError + + def processComment(self, token): + # For most phases the following is correct. Where it's not it will be + # overridden. + self.tree.insertComment(token, self.tree.openElements[-1]) + + def processDoctype(self, token): + self.parser.parseError("unexpected-doctype") + + def processCharacters(self, token): + self.tree.insertText(token["data"]) + + def processSpaceCharacters(self, token): + self.tree.insertText(token["data"]) + + def processStartTag(self, token): + return self.startTagHandler[token["name"]](token) + + def startTagHtml(self, token): + if not self.parser.firstStartTag and token["name"] == "html": + self.parser.parseError("non-html-root") + # XXX Need a check here to see if the first start tag token emitted is + # this token... If it's not, invoke self.parser.parseError(). + for attr, value in token["data"].items(): + if attr not in self.tree.openElements[0].attributes: + self.tree.openElements[0].attributes[attr] = value + self.parser.firstStartTag = False + + def processEndTag(self, token): + return self.endTagHandler[token["name"]](token) + + class InitialPhase(Phase): + def processSpaceCharacters(self, token): + pass + + def processComment(self, token): + self.tree.insertComment(token, self.tree.document) + + def processDoctype(self, token): + name = token["name"] + publicId = token["publicId"] + systemId = token["systemId"] + correct = token["correct"] + + if (name != "html" or publicId is not None or + systemId is not None and systemId != "about:legacy-compat"): + self.parser.parseError("unknown-doctype") + + if publicId is None: + publicId = "" + + self.tree.insertDoctype(token) + + if publicId != "": + publicId = publicId.translate(asciiUpper2Lower) + + if (not correct or token["name"] != "html" or + publicId.startswith( + ("+//silmaril//dtd html pro v0r11 19970101//", + "-//advasoft ltd//dtd html 3.0 aswedit + extensions//", + "-//as//dtd html 3.0 aswedit + extensions//", + "-//ietf//dtd html 2.0 level 1//", + "-//ietf//dtd html 2.0 level 2//", + "-//ietf//dtd html 2.0 strict level 1//", + "-//ietf//dtd html 2.0 strict level 2//", + "-//ietf//dtd html 2.0 strict//", + "-//ietf//dtd html 2.0//", + "-//ietf//dtd html 2.1e//", + "-//ietf//dtd html 3.0//", + "-//ietf//dtd html 3.2 final//", + "-//ietf//dtd html 3.2//", + "-//ietf//dtd html 3//", + "-//ietf//dtd html level 0//", + "-//ietf//dtd html level 1//", + "-//ietf//dtd html level 2//", + "-//ietf//dtd html level 3//", + "-//ietf//dtd html strict level 0//", + "-//ietf//dtd html strict level 1//", + "-//ietf//dtd html strict level 2//", + "-//ietf//dtd html strict level 3//", + "-//ietf//dtd html strict//", + "-//ietf//dtd html//", + "-//metrius//dtd metrius presentational//", + "-//microsoft//dtd internet explorer 2.0 html strict//", + "-//microsoft//dtd internet explorer 2.0 html//", + "-//microsoft//dtd internet explorer 2.0 tables//", + "-//microsoft//dtd internet explorer 3.0 html strict//", + "-//microsoft//dtd internet explorer 3.0 html//", + "-//microsoft//dtd internet explorer 3.0 tables//", + "-//netscape comm. corp.//dtd html//", + "-//netscape comm. corp.//dtd strict html//", + "-//o'reilly and associates//dtd html 2.0//", + "-//o'reilly and associates//dtd html extended 1.0//", + "-//o'reilly and associates//dtd html extended relaxed 1.0//", + "-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//", + "-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//", + "-//spyglass//dtd html 2.0 extended//", + "-//sq//dtd html 2.0 hotmetal + extensions//", + "-//sun microsystems corp.//dtd hotjava html//", + "-//sun microsystems corp.//dtd hotjava strict html//", + "-//w3c//dtd html 3 1995-03-24//", + "-//w3c//dtd html 3.2 draft//", + "-//w3c//dtd html 3.2 final//", + "-//w3c//dtd html 3.2//", + "-//w3c//dtd html 3.2s draft//", + "-//w3c//dtd html 4.0 frameset//", + "-//w3c//dtd html 4.0 transitional//", + "-//w3c//dtd html experimental 19960712//", + "-//w3c//dtd html experimental 970421//", + "-//w3c//dtd w3 html//", + "-//w3o//dtd w3 html 3.0//", + "-//webtechs//dtd mozilla html 2.0//", + "-//webtechs//dtd mozilla html//")) or + publicId in ("-//w3o//dtd w3 html strict 3.0//en//", + "-/w3c/dtd html 4.0 transitional/en", + "html") or + publicId.startswith( + ("-//w3c//dtd html 4.01 frameset//", + "-//w3c//dtd html 4.01 transitional//")) and + systemId is None or + systemId and systemId.lower() == "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd"): + self.parser.compatMode = "quirks" + elif (publicId.startswith( + ("-//w3c//dtd xhtml 1.0 frameset//", + "-//w3c//dtd xhtml 1.0 transitional//")) or + publicId.startswith( + ("-//w3c//dtd html 4.01 frameset//", + "-//w3c//dtd html 4.01 transitional//")) and + systemId is not None): + self.parser.compatMode = "limited quirks" + + self.parser.phase = self.parser.phases["beforeHtml"] + + def anythingElse(self): + self.parser.compatMode = "quirks" + self.parser.phase = self.parser.phases["beforeHtml"] + + def processCharacters(self, token): + self.parser.parseError("expected-doctype-but-got-chars") + self.anythingElse() + return token + + def processStartTag(self, token): + self.parser.parseError("expected-doctype-but-got-start-tag", + {"name": token["name"]}) + self.anythingElse() + return token + + def processEndTag(self, token): + self.parser.parseError("expected-doctype-but-got-end-tag", + {"name": token["name"]}) + self.anythingElse() + return token + + def processEOF(self): + self.parser.parseError("expected-doctype-but-got-eof") + self.anythingElse() + return True + + class BeforeHtmlPhase(Phase): + # helper methods + def insertHtmlElement(self): + self.tree.insertRoot(impliedTagToken("html", "StartTag")) + self.parser.phase = self.parser.phases["beforeHead"] + + # other + def processEOF(self): + self.insertHtmlElement() + return True + + def processComment(self, token): + self.tree.insertComment(token, self.tree.document) + + def processSpaceCharacters(self, token): + pass + + def processCharacters(self, token): + self.insertHtmlElement() + return token + + def processStartTag(self, token): + if token["name"] == "html": + self.parser.firstStartTag = True + self.insertHtmlElement() + return token + + def processEndTag(self, token): + if token["name"] not in ("head", "body", "html", "br"): + self.parser.parseError("unexpected-end-tag-before-html", + {"name": token["name"]}) + else: + self.insertHtmlElement() + return token + + class BeforeHeadPhase(Phase): + def __init__(self, parser, tree): + Phase.__init__(self, parser, tree) + + self.startTagHandler = _utils.MethodDispatcher([ + ("html", self.startTagHtml), + ("head", self.startTagHead) + ]) + self.startTagHandler.default = self.startTagOther + + self.endTagHandler = _utils.MethodDispatcher([ + (("head", "body", "html", "br"), self.endTagImplyHead) + ]) + self.endTagHandler.default = self.endTagOther + + def processEOF(self): + self.startTagHead(impliedTagToken("head", "StartTag")) + return True + + def processSpaceCharacters(self, token): + pass + + def processCharacters(self, token): + self.startTagHead(impliedTagToken("head", "StartTag")) + return token + + def startTagHtml(self, token): + return self.parser.phases["inBody"].processStartTag(token) + + def startTagHead(self, token): + self.tree.insertElement(token) + self.tree.headPointer = self.tree.openElements[-1] + self.parser.phase = self.parser.phases["inHead"] + + def startTagOther(self, token): + self.startTagHead(impliedTagToken("head", "StartTag")) + return token + + def endTagImplyHead(self, token): + self.startTagHead(impliedTagToken("head", "StartTag")) + return token + + def endTagOther(self, token): + self.parser.parseError("end-tag-after-implied-root", + {"name": token["name"]}) + + class InHeadPhase(Phase): + def __init__(self, parser, tree): + Phase.__init__(self, parser, tree) + + self.startTagHandler = _utils.MethodDispatcher([ + ("html", self.startTagHtml), + ("title", self.startTagTitle), + (("noframes", "style"), self.startTagNoFramesStyle), + ("noscript", self.startTagNoscript), + ("script", self.startTagScript), + (("base", "basefont", "bgsound", "command", "link"), + self.startTagBaseLinkCommand), + ("meta", self.startTagMeta), + ("head", self.startTagHead) + ]) + self.startTagHandler.default = self.startTagOther + + self.endTagHandler = _utils.MethodDispatcher([ + ("head", self.endTagHead), + (("br", "html", "body"), self.endTagHtmlBodyBr) + ]) + self.endTagHandler.default = self.endTagOther + + # the real thing + def processEOF(self): + self.anythingElse() + return True + + def processCharacters(self, token): + self.anythingElse() + return token + + def startTagHtml(self, token): + return self.parser.phases["inBody"].processStartTag(token) + + def startTagHead(self, token): + self.parser.parseError("two-heads-are-not-better-than-one") + + def startTagBaseLinkCommand(self, token): + self.tree.insertElement(token) + self.tree.openElements.pop() + token["selfClosingAcknowledged"] = True + + def startTagMeta(self, token): + self.tree.insertElement(token) + self.tree.openElements.pop() + token["selfClosingAcknowledged"] = True + + attributes = token["data"] + if self.parser.tokenizer.stream.charEncoding[1] == "tentative": + if "charset" in attributes: + self.parser.tokenizer.stream.changeEncoding(attributes["charset"]) + elif ("content" in attributes and + "http-equiv" in attributes and + attributes["http-equiv"].lower() == "content-type"): + # Encoding it as UTF-8 here is a hack, as really we should pass + # the abstract Unicode string, and just use the + # ContentAttrParser on that, but using UTF-8 allows all chars + # to be encoded and as a ASCII-superset works. + data = _inputstream.EncodingBytes(attributes["content"].encode("utf-8")) + parser = _inputstream.ContentAttrParser(data) + codec = parser.parse() + self.parser.tokenizer.stream.changeEncoding(codec) + + def startTagTitle(self, token): + self.parser.parseRCDataRawtext(token, "RCDATA") + + def startTagNoFramesStyle(self, token): + # Need to decide whether to implement the scripting-disabled case + self.parser.parseRCDataRawtext(token, "RAWTEXT") + + def startTagNoscript(self, token): + if self.parser.scripting: + self.parser.parseRCDataRawtext(token, "RAWTEXT") + else: + self.tree.insertElement(token) + self.parser.phase = self.parser.phases["inHeadNoscript"] + + def startTagScript(self, token): + self.tree.insertElement(token) + self.parser.tokenizer.state = self.parser.tokenizer.scriptDataState + self.parser.originalPhase = self.parser.phase + self.parser.phase = self.parser.phases["text"] + + def startTagOther(self, token): + self.anythingElse() + return token + + def endTagHead(self, token): + node = self.parser.tree.openElements.pop() + assert node.name == "head", "Expected head got %s" % node.name + self.parser.phase = self.parser.phases["afterHead"] + + def endTagHtmlBodyBr(self, token): + self.anythingElse() + return token + + def endTagOther(self, token): + self.parser.parseError("unexpected-end-tag", {"name": token["name"]}) + + def anythingElse(self): + self.endTagHead(impliedTagToken("head")) + + class InHeadNoscriptPhase(Phase): + def __init__(self, parser, tree): + Phase.__init__(self, parser, tree) + + self.startTagHandler = _utils.MethodDispatcher([ + ("html", self.startTagHtml), + (("basefont", "bgsound", "link", "meta", "noframes", "style"), self.startTagBaseLinkCommand), + (("head", "noscript"), self.startTagHeadNoscript), + ]) + self.startTagHandler.default = self.startTagOther + + self.endTagHandler = _utils.MethodDispatcher([ + ("noscript", self.endTagNoscript), + ("br", self.endTagBr), + ]) + self.endTagHandler.default = self.endTagOther + + def processEOF(self): + self.parser.parseError("eof-in-head-noscript") + self.anythingElse() + return True + + def processComment(self, token): + return self.parser.phases["inHead"].processComment(token) + + def processCharacters(self, token): + self.parser.parseError("char-in-head-noscript") + self.anythingElse() + return token + + def processSpaceCharacters(self, token): + return self.parser.phases["inHead"].processSpaceCharacters(token) + + def startTagHtml(self, token): + return self.parser.phases["inBody"].processStartTag(token) + + def startTagBaseLinkCommand(self, token): + return self.parser.phases["inHead"].processStartTag(token) + + def startTagHeadNoscript(self, token): + self.parser.parseError("unexpected-start-tag", {"name": token["name"]}) + + def startTagOther(self, token): + self.parser.parseError("unexpected-inhead-noscript-tag", {"name": token["name"]}) + self.anythingElse() + return token + + def endTagNoscript(self, token): + node = self.parser.tree.openElements.pop() + assert node.name == "noscript", "Expected noscript got %s" % node.name + self.parser.phase = self.parser.phases["inHead"] + + def endTagBr(self, token): + self.parser.parseError("unexpected-inhead-noscript-tag", {"name": token["name"]}) + self.anythingElse() + return token + + def endTagOther(self, token): + self.parser.parseError("unexpected-end-tag", {"name": token["name"]}) + + def anythingElse(self): + # Caller must raise parse error first! + self.endTagNoscript(impliedTagToken("noscript")) + + class AfterHeadPhase(Phase): + def __init__(self, parser, tree): + Phase.__init__(self, parser, tree) + + self.startTagHandler = _utils.MethodDispatcher([ + ("html", self.startTagHtml), + ("body", self.startTagBody), + ("frameset", self.startTagFrameset), + (("base", "basefont", "bgsound", "link", "meta", "noframes", "script", + "style", "title"), + self.startTagFromHead), + ("head", self.startTagHead) + ]) + self.startTagHandler.default = self.startTagOther + self.endTagHandler = _utils.MethodDispatcher([(("body", "html", "br"), + self.endTagHtmlBodyBr)]) + self.endTagHandler.default = self.endTagOther + + def processEOF(self): + self.anythingElse() + return True + + def processCharacters(self, token): + self.anythingElse() + return token + + def startTagHtml(self, token): + return self.parser.phases["inBody"].processStartTag(token) + + def startTagBody(self, token): + self.parser.framesetOK = False + self.tree.insertElement(token) + self.parser.phase = self.parser.phases["inBody"] + + def startTagFrameset(self, token): + self.tree.insertElement(token) + self.parser.phase = self.parser.phases["inFrameset"] + + def startTagFromHead(self, token): + self.parser.parseError("unexpected-start-tag-out-of-my-head", + {"name": token["name"]}) + self.tree.openElements.append(self.tree.headPointer) + self.parser.phases["inHead"].processStartTag(token) + for node in self.tree.openElements[::-1]: + if node.name == "head": + self.tree.openElements.remove(node) + break + + def startTagHead(self, token): + self.parser.parseError("unexpected-start-tag", {"name": token["name"]}) + + def startTagOther(self, token): + self.anythingElse() + return token + + def endTagHtmlBodyBr(self, token): + self.anythingElse() + return token + + def endTagOther(self, token): + self.parser.parseError("unexpected-end-tag", {"name": token["name"]}) + + def anythingElse(self): + self.tree.insertElement(impliedTagToken("body", "StartTag")) + self.parser.phase = self.parser.phases["inBody"] + self.parser.framesetOK = True + + class InBodyPhase(Phase): + # http://www.whatwg.org/specs/web-apps/current-work/#parsing-main-inbody + # the really-really-really-very crazy mode + def __init__(self, parser, tree): + Phase.__init__(self, parser, tree) + + # Set this to the default handler + self.processSpaceCharacters = self.processSpaceCharactersNonPre + + self.startTagHandler = _utils.MethodDispatcher([ + ("html", self.startTagHtml), + (("base", "basefont", "bgsound", "command", "link", "meta", + "script", "style", "title"), + self.startTagProcessInHead), + ("body", self.startTagBody), + ("frameset", self.startTagFrameset), + (("address", "article", "aside", "blockquote", "center", "details", + "dir", "div", "dl", "fieldset", "figcaption", "figure", + "footer", "header", "hgroup", "main", "menu", "nav", "ol", "p", + "section", "summary", "ul"), + self.startTagCloseP), + (headingElements, self.startTagHeading), + (("pre", "listing"), self.startTagPreListing), + ("form", self.startTagForm), + (("li", "dd", "dt"), self.startTagListItem), + ("plaintext", self.startTagPlaintext), + ("a", self.startTagA), + (("b", "big", "code", "em", "font", "i", "s", "small", "strike", + "strong", "tt", "u"), self.startTagFormatting), + ("nobr", self.startTagNobr), + ("button", self.startTagButton), + (("applet", "marquee", "object"), self.startTagAppletMarqueeObject), + ("xmp", self.startTagXmp), + ("table", self.startTagTable), + (("area", "br", "embed", "img", "keygen", "wbr"), + self.startTagVoidFormatting), + (("param", "source", "track"), self.startTagParamSource), + ("input", self.startTagInput), + ("hr", self.startTagHr), + ("image", self.startTagImage), + ("isindex", self.startTagIsIndex), + ("textarea", self.startTagTextarea), + ("iframe", self.startTagIFrame), + ("noscript", self.startTagNoscript), + (("noembed", "noframes"), self.startTagRawtext), + ("select", self.startTagSelect), + (("rp", "rt"), self.startTagRpRt), + (("option", "optgroup"), self.startTagOpt), + (("math"), self.startTagMath), + (("svg"), self.startTagSvg), + (("caption", "col", "colgroup", "frame", "head", + "tbody", "td", "tfoot", "th", "thead", + "tr"), self.startTagMisplaced) + ]) + self.startTagHandler.default = self.startTagOther + + self.endTagHandler = _utils.MethodDispatcher([ + ("body", self.endTagBody), + ("html", self.endTagHtml), + (("address", "article", "aside", "blockquote", "button", "center", + "details", "dialog", "dir", "div", "dl", "fieldset", "figcaption", "figure", + "footer", "header", "hgroup", "listing", "main", "menu", "nav", "ol", "pre", + "section", "summary", "ul"), self.endTagBlock), + ("form", self.endTagForm), + ("p", self.endTagP), + (("dd", "dt", "li"), self.endTagListItem), + (headingElements, self.endTagHeading), + (("a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", + "strike", "strong", "tt", "u"), self.endTagFormatting), + (("applet", "marquee", "object"), self.endTagAppletMarqueeObject), + ("br", self.endTagBr), + ]) + self.endTagHandler.default = self.endTagOther + + def isMatchingFormattingElement(self, node1, node2): + return (node1.name == node2.name and + node1.namespace == node2.namespace and + node1.attributes == node2.attributes) + + # helper + def addFormattingElement(self, token): + self.tree.insertElement(token) + element = self.tree.openElements[-1] + + matchingElements = [] + for node in self.tree.activeFormattingElements[::-1]: + if node is Marker: + break + elif self.isMatchingFormattingElement(node, element): + matchingElements.append(node) + + assert len(matchingElements) <= 3 + if len(matchingElements) == 3: + self.tree.activeFormattingElements.remove(matchingElements[-1]) + self.tree.activeFormattingElements.append(element) + + # the real deal + def processEOF(self): + allowed_elements = frozenset(("dd", "dt", "li", "p", "tbody", "td", + "tfoot", "th", "thead", "tr", "body", + "html")) + for node in self.tree.openElements[::-1]: + if node.name not in allowed_elements: + self.parser.parseError("expected-closing-tag-but-got-eof") + break + # Stop parsing + + def processSpaceCharactersDropNewline(self, token): + # Sometimes (start of
, , and 
+
+
+ The debugger caught an exception in your WSGI application. You can now + look at the traceback which led to the error. + If you enable JavaScript you can also use additional features such as code + execution (if the evalex feature is enabled), automatic pasting of the + exceptions and much more. +
+""" + + FOOTER + + """ + +""" +) + +CONSOLE_HTML = ( + HEADER + + """\ +

Interactive Console

+
+In this console you can execute Python expressions in the context of the +application. The initial namespace was created by the debugger automatically. +
+
The Console requires JavaScript.
+""" + + FOOTER +) + +SUMMARY_HTML = """\ +
+ %(title)s +
    %(frames)s
+ %(description)s +
+""" + +FRAME_HTML = """\ +
+

File "%(filename)s", + line %(lineno)s, + in %(function_name)s

+
%(lines)s
+
+""" + +SOURCE_LINE_HTML = """\ + + %(lineno)s + %(code)s + +""" + + +def render_console_html(secret: str, evalex_trusted: bool = True) -> str: + return CONSOLE_HTML % { + "evalex": "true", + "evalex_trusted": "true" if evalex_trusted else "false", + "console": "true", + "title": "Console", + "secret": secret, + "traceback_id": -1, + } + + +def get_current_traceback( + ignore_system_exceptions: bool = False, + show_hidden_frames: bool = False, + skip: int = 0, +) -> "Traceback": + """Get the current exception info as `Traceback` object. Per default + calling this method will reraise system exceptions such as generator exit, + system exit or others. This behavior can be disabled by passing `False` + to the function as first parameter. + """ + info = t.cast( + t.Tuple[t.Type[BaseException], BaseException, TracebackType], sys.exc_info() + ) + exc_type, exc_value, tb = info + + if ignore_system_exceptions and exc_type in { + SystemExit, + KeyboardInterrupt, + GeneratorExit, + }: + raise + for _ in range(skip): + if tb.tb_next is None: + break + tb = tb.tb_next + tb = Traceback(exc_type, exc_value, tb) + if not show_hidden_frames: + tb.filter_hidden_frames() + return tb + + +class Line: + """Helper for the source renderer.""" + + __slots__ = ("lineno", "code", "in_frame", "current") + + def __init__(self, lineno: int, code: str) -> None: + self.lineno = lineno + self.code = code + self.in_frame = False + self.current = False + + @property + def classes(self) -> t.List[str]: + rv = ["line"] + if self.in_frame: + rv.append("in-frame") + if self.current: + rv.append("current") + return rv + + def render(self) -> str: + return SOURCE_LINE_HTML % { + "classes": " ".join(self.classes), + "lineno": self.lineno, + "code": escape(self.code), + } + + +class Traceback: + """Wraps a traceback.""" + + def __init__( + self, + exc_type: t.Type[BaseException], + exc_value: BaseException, + tb: TracebackType, + ) -> None: + self.exc_type = exc_type + self.exc_value = exc_value + self.tb = tb + + exception_type = exc_type.__name__ + if exc_type.__module__ not in {"builtins", "__builtin__", "exceptions"}: + exception_type = f"{exc_type.__module__}.{exception_type}" + self.exception_type = exception_type + + self.groups = [] + memo = set() + while True: + self.groups.append(Group(exc_type, exc_value, tb)) + memo.add(id(exc_value)) + exc_value = exc_value.__cause__ or exc_value.__context__ # type: ignore + if exc_value is None or id(exc_value) in memo: + break + exc_type = type(exc_value) + tb = exc_value.__traceback__ # type: ignore + self.groups.reverse() + self.frames = [frame for group in self.groups for frame in group.frames] + + def filter_hidden_frames(self) -> None: + """Remove the frames according to the paste spec.""" + for group in self.groups: + group.filter_hidden_frames() + + self.frames[:] = [frame for group in self.groups for frame in group.frames] + + @property + def is_syntax_error(self) -> bool: + """Is it a syntax error?""" + return isinstance(self.exc_value, SyntaxError) + + @property + def exception(self) -> str: + """String representation of the final exception.""" + return self.groups[-1].exception + + def log(self, logfile: t.Optional[t.IO[str]] = None) -> None: + """Log the ASCII traceback into a file object.""" + if logfile is None: + logfile = sys.stderr + tb = f"{self.plaintext.rstrip()}\n" + logfile.write(tb) + + def render_summary(self, include_title: bool = True) -> str: + """Render the traceback for the interactive console.""" + title = "" + classes = ["traceback"] + if not self.frames: + classes.append("noframe-traceback") + frames = [] + else: + library_frames = sum(frame.is_library for frame in self.frames) + mark_lib = 0 < library_frames < len(self.frames) + frames = [group.render(mark_lib=mark_lib) for group in self.groups] + + if include_title: + if self.is_syntax_error: + title = "Syntax Error" + else: + title = "Traceback (most recent call last):" + + if self.is_syntax_error: + description = f"
{escape(self.exception)}
" + else: + description = f"
{escape(self.exception)}
" + + return SUMMARY_HTML % { + "classes": " ".join(classes), + "title": f"

{title if title else ''}

", + "frames": "\n".join(frames), + "description": description, + } + + def render_full( + self, + evalex: bool = False, + secret: t.Optional[str] = None, + evalex_trusted: bool = True, + ) -> str: + """Render the Full HTML page with the traceback info.""" + exc = escape(self.exception) + return PAGE_HTML % { + "evalex": "true" if evalex else "false", + "evalex_trusted": "true" if evalex_trusted else "false", + "console": "false", + "title": exc, + "exception": exc, + "exception_type": escape(self.exception_type), + "summary": self.render_summary(include_title=False), + "plaintext": escape(self.plaintext), + "plaintext_cs": re.sub("-{2,}", "-", self.plaintext), + "traceback_id": self.id, + "secret": secret, + } + + @cached_property + def plaintext(self) -> str: + return "\n".join([group.render_text() for group in self.groups]) + + @property + def id(self) -> int: + return id(self) + + +class Group: + """A group of frames for an exception in a traceback. If the + exception has a ``__cause__`` or ``__context__``, there are multiple + exception groups. + """ + + def __init__( + self, + exc_type: t.Type[BaseException], + exc_value: BaseException, + tb: TracebackType, + ) -> None: + self.exc_type = exc_type + self.exc_value = exc_value + self.info = None + if exc_value.__cause__ is not None: + self.info = ( + "The above exception was the direct cause of the following exception" + ) + elif exc_value.__context__ is not None: + self.info = ( + "During handling of the above exception, another exception occurred" + ) + + self.frames = [] + while tb is not None: + self.frames.append(Frame(exc_type, exc_value, tb)) + tb = tb.tb_next # type: ignore + + def filter_hidden_frames(self) -> None: + # An exception may not have a traceback to filter frames, such + # as one re-raised from ProcessPoolExecutor. + if not self.frames: + return + + new_frames: t.List[Frame] = [] + hidden = False + + for frame in self.frames: + hide = frame.hide + if hide in ("before", "before_and_this"): + new_frames = [] + hidden = False + if hide == "before_and_this": + continue + elif hide in ("reset", "reset_and_this"): + hidden = False + if hide == "reset_and_this": + continue + elif hide in ("after", "after_and_this"): + hidden = True + if hide == "after_and_this": + continue + elif hide or hidden: + continue + new_frames.append(frame) + + # if we only have one frame and that frame is from the codeop + # module, remove it. + if len(new_frames) == 1 and self.frames[0].module == "codeop": + del self.frames[:] + + # if the last frame is missing something went terrible wrong :( + elif self.frames[-1] in new_frames: + self.frames[:] = new_frames + + @property + def exception(self) -> str: + """String representation of the exception.""" + buf = traceback.format_exception_only(self.exc_type, self.exc_value) + rv = "".join(buf).strip() + return _to_str(rv, "utf-8", "replace") + + def render(self, mark_lib: bool = True) -> str: + out = [] + if self.info is not None: + out.append(f'
  • {self.info}:
    ') + for frame in self.frames: + title = f' title="{escape(frame.info)}"' if frame.info else "" + out.append(f"{frame.render(mark_lib=mark_lib)}") + return "\n".join(out) + + def render_text(self) -> str: + out = [] + if self.info is not None: + out.append(f"\n{self.info}:\n") + out.append("Traceback (most recent call last):") + for frame in self.frames: + out.append(frame.render_text()) + out.append(self.exception) + return "\n".join(out) + + +class Frame: + """A single frame in a traceback.""" + + def __init__( + self, + exc_type: t.Type[BaseException], + exc_value: BaseException, + tb: TracebackType, + ) -> None: + self.lineno = tb.tb_lineno + self.function_name = tb.tb_frame.f_code.co_name + self.locals = tb.tb_frame.f_locals + self.globals = tb.tb_frame.f_globals + + fn = inspect.getsourcefile(tb) or inspect.getfile(tb) + if fn[-4:] in (".pyo", ".pyc"): + fn = fn[:-1] + # if it's a file on the file system resolve the real filename. + if os.path.isfile(fn): + fn = os.path.realpath(fn) + self.filename = _to_str(fn, get_filesystem_encoding()) + self.module = self.globals.get("__name__", self.locals.get("__name__")) + self.loader = self.globals.get("__loader__", self.locals.get("__loader__")) + self.code = tb.tb_frame.f_code + + # support for paste's traceback extensions + self.hide = self.locals.get("__traceback_hide__", False) + info = self.locals.get("__traceback_info__") + if info is not None: + info = _to_str(info, "utf-8", "replace") + self.info = info + + def render(self, mark_lib: bool = True) -> str: + """Render a single frame in a traceback.""" + return FRAME_HTML % { + "id": self.id, + "filename": escape(self.filename), + "lineno": self.lineno, + "function_name": escape(self.function_name), + "lines": self.render_line_context(), + "library": "library" if mark_lib and self.is_library else "", + } + + @cached_property + def is_library(self) -> bool: + return any( + self.filename.startswith(os.path.realpath(path)) + for path in sysconfig.get_paths().values() + ) + + def render_text(self) -> str: + return ( + f' File "{self.filename}", line {self.lineno}, in {self.function_name}\n' + f" {self.current_line.strip()}" + ) + + def render_line_context(self) -> str: + before, current, after = self.get_context_lines() + rv = [] + + def render_line(line: str, cls: str) -> None: + line = line.expandtabs().rstrip() + stripped_line = line.strip() + prefix = len(line) - len(stripped_line) + rv.append( + f'
    {" " * prefix}'
    +                f"{escape(stripped_line) if stripped_line else ' '}
    " + ) + + for line in before: + render_line(line, "before") + render_line(current, "current") + for line in after: + render_line(line, "after") + + return "\n".join(rv) + + def get_annotated_lines(self) -> t.List[Line]: + """Helper function that returns lines with extra information.""" + lines = [Line(idx + 1, x) for idx, x in enumerate(self.sourcelines)] + + # find function definition and mark lines + if hasattr(self.code, "co_firstlineno"): + lineno = self.code.co_firstlineno - 1 + while lineno > 0: + if _funcdef_re.match(lines[lineno].code): + break + lineno -= 1 + try: + offset = len(inspect.getblock([f"{x.code}\n" for x in lines[lineno:]])) + except TokenError: + offset = 0 + for line in lines[lineno : lineno + offset]: + line.in_frame = True + + # mark current line + try: + lines[self.lineno - 1].current = True + except IndexError: + pass + + return lines + + def eval(self, code: t.Union[str, CodeType], mode: str = "single") -> t.Any: + """Evaluate code in the context of the frame.""" + if isinstance(code, str): + code = compile(code, "", mode) + return eval(code, self.globals, self.locals) + + @cached_property + def sourcelines(self) -> t.List[str]: + """The sourcecode of the file as list of strings.""" + # get sourcecode from loader or file + source = None + if self.loader is not None: + try: + if hasattr(self.loader, "get_source"): + source = self.loader.get_source(self.module) + elif hasattr(self.loader, "get_source_by_code"): + source = self.loader.get_source_by_code(self.code) + except Exception: + # we munch the exception so that we don't cause troubles + # if the loader is broken. + pass + + if source is None: + try: + with open(self.filename, mode="rb") as f: + source = f.read() + except OSError: + return [] + + # already str? return right away + if isinstance(source, str): + return source.splitlines() + + charset = "utf-8" + if source.startswith(codecs.BOM_UTF8): + source = source[3:] + else: + for idx, match in enumerate(_line_re.finditer(source)): + coding_match = _coding_re.search(match.group()) + if coding_match is not None: + charset = coding_match.group(1).decode("utf-8") + break + if idx > 1: + break + + # on broken cookies we fall back to utf-8 too + charset = _to_str(charset) + try: + codecs.lookup(charset) + except LookupError: + charset = "utf-8" + + return source.decode(charset, "replace").splitlines() + + def get_context_lines( + self, context: int = 5 + ) -> t.Tuple[t.List[str], str, t.List[str]]: + before = self.sourcelines[self.lineno - context - 1 : self.lineno - 1] + past = self.sourcelines[self.lineno : self.lineno + context] + return (before, self.current_line, past) + + @property + def current_line(self) -> str: + try: + return self.sourcelines[self.lineno - 1] + except IndexError: + return "" + + @cached_property + def console(self) -> Console: + return Console(self.globals, self.locals) + + @property + def id(self) -> int: + return id(self) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/exceptions.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/exceptions.py new file mode 100644 index 000000000..16c3964d2 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/exceptions.py @@ -0,0 +1,943 @@ +"""Implements a number of Python exceptions which can be raised from within +a view to trigger a standard HTTP non-200 response. + +Usage Example +------------- + +.. code-block:: python + + from werkzeug.wrappers.request import Request + from werkzeug.exceptions import HTTPException, NotFound + + def view(request): + raise NotFound() + + @Request.application + def application(request): + try: + return view(request) + except HTTPException as e: + return e + +As you can see from this example those exceptions are callable WSGI +applications. However, they are not Werkzeug response objects. You +can get a response object by calling ``get_response()`` on a HTTP +exception. + +Keep in mind that you may have to pass an environ (WSGI) or scope +(ASGI) to ``get_response()`` because some errors fetch additional +information relating to the request. + +If you want to hook in a different exception page to say, a 404 status +code, you can add a second except for a specific subclass of an error: + +.. code-block:: python + + @Request.application + def application(request): + try: + return view(request) + except NotFound as e: + return not_found(request) + except HTTPException as e: + return e + +""" +import sys +import typing as t +import warnings +from datetime import datetime +from html import escape + +from ._internal import _get_environ + +if t.TYPE_CHECKING: + import typing_extensions as te + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIEnvironment + from .datastructures import WWWAuthenticate + from .sansio.response import Response + from .wrappers.response import Response as WSGIResponse # noqa: F401 + + +class HTTPException(Exception): + """The base class for all HTTP exceptions. This exception can be called as a WSGI + application to render a default error page or you can catch the subclasses + of it independently and render nicer error messages. + """ + + code: t.Optional[int] = None + description: t.Optional[str] = None + + def __init__( + self, + description: t.Optional[str] = None, + response: t.Optional["Response"] = None, + ) -> None: + super().__init__() + if description is not None: + self.description = description + self.response = response + + @classmethod + def wrap( + cls, exception: t.Type[BaseException], name: t.Optional[str] = None + ) -> t.Type["HTTPException"]: + """Create an exception that is a subclass of the calling HTTP + exception and the ``exception`` argument. + + The first argument to the class will be passed to the + wrapped ``exception``, the rest to the HTTP exception. If + ``e.args`` is not empty and ``e.show_exception`` is ``True``, + the wrapped exception message is added to the HTTP error + description. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Create a subclass manually + instead. + + .. versionchanged:: 0.15.5 + The ``show_exception`` attribute controls whether the + description includes the wrapped exception message. + + .. versionchanged:: 0.15.0 + The description includes the wrapped exception message. + """ + warnings.warn( + "'HTTPException.wrap' is deprecated and will be removed in" + " Werkzeug 2.1. Create a subclass manually instead.", + DeprecationWarning, + stacklevel=2, + ) + + class newcls(cls, exception): # type: ignore + _description = cls.description + show_exception = False + + def __init__( + self, arg: t.Optional[t.Any] = None, *args: t.Any, **kwargs: t.Any + ) -> None: + super().__init__(*args, **kwargs) + + if arg is None: + exception.__init__(self) + else: + exception.__init__(self, arg) + + @property + def description(self) -> str: + if self.show_exception: + return ( + f"{self._description}\n" + f"{exception.__name__}: {exception.__str__(self)}" + ) + + return self._description # type: ignore + + @description.setter + def description(self, value: str) -> None: + self._description = value + + newcls.__module__ = sys._getframe(1).f_globals["__name__"] + name = name or cls.__name__ + exception.__name__ + newcls.__name__ = newcls.__qualname__ = name + return newcls + + @property + def name(self) -> str: + """The status name.""" + from .http import HTTP_STATUS_CODES + + return HTTP_STATUS_CODES.get(self.code, "Unknown Error") # type: ignore + + def get_description( + self, + environ: t.Optional["WSGIEnvironment"] = None, + scope: t.Optional[dict] = None, + ) -> str: + """Get the description.""" + if self.description is None: + description = "" + elif not isinstance(self.description, str): + description = str(self.description) + else: + description = self.description + + description = escape(description).replace("\n", "
    ") + return f"

    {description}

    " + + def get_body( + self, + environ: t.Optional["WSGIEnvironment"] = None, + scope: t.Optional[dict] = None, + ) -> str: + """Get the HTML body.""" + return ( + '\n' + f"{self.code} {escape(self.name)}\n" + f"

    {escape(self.name)}

    \n" + f"{self.get_description(environ)}\n" + ) + + def get_headers( + self, + environ: t.Optional["WSGIEnvironment"] = None, + scope: t.Optional[dict] = None, + ) -> t.List[t.Tuple[str, str]]: + """Get a list of headers.""" + return [("Content-Type", "text/html; charset=utf-8")] + + def get_response( + self, + environ: t.Optional["WSGIEnvironment"] = None, + scope: t.Optional[dict] = None, + ) -> "Response": + """Get a response object. If one was passed to the exception + it's returned directly. + + :param environ: the optional environ for the request. This + can be used to modify the response depending + on how the request looked like. + :return: a :class:`Response` object or a subclass thereof. + """ + from .wrappers.response import Response as WSGIResponse # noqa: F811 + + if self.response is not None: + return self.response + if environ is not None: + environ = _get_environ(environ) + headers = self.get_headers(environ, scope) + return WSGIResponse(self.get_body(environ, scope), self.code, headers) + + def __call__( + self, environ: "WSGIEnvironment", start_response: "StartResponse" + ) -> t.Iterable[bytes]: + """Call the exception as WSGI application. + + :param environ: the WSGI environment. + :param start_response: the response callable provided by the WSGI + server. + """ + response = t.cast("WSGIResponse", self.get_response(environ)) + return response(environ, start_response) + + def __str__(self) -> str: + code = self.code if self.code is not None else "???" + return f"{code} {self.name}: {self.description}" + + def __repr__(self) -> str: + code = self.code if self.code is not None else "???" + return f"<{type(self).__name__} '{code}: {self.name}'>" + + +class BadRequest(HTTPException): + """*400* `Bad Request` + + Raise if the browser sends something to the application the application + or server cannot handle. + """ + + code = 400 + description = ( + "The browser (or proxy) sent a request that this server could " + "not understand." + ) + + +class BadRequestKeyError(BadRequest, KeyError): + """An exception that is used to signal both a :exc:`KeyError` and a + :exc:`BadRequest`. Used by many of the datastructures. + """ + + _description = BadRequest.description + #: Show the KeyError along with the HTTP error message in the + #: response. This should be disabled in production, but can be + #: useful in a debug mode. + show_exception = False + + def __init__(self, arg: t.Optional[str] = None, *args: t.Any, **kwargs: t.Any): + super().__init__(*args, **kwargs) + + if arg is None: + KeyError.__init__(self) + else: + KeyError.__init__(self, arg) + + @property # type: ignore + def description(self) -> str: # type: ignore + if self.show_exception: + return ( + f"{self._description}\n" + f"{KeyError.__name__}: {KeyError.__str__(self)}" + ) + + return self._description + + @description.setter + def description(self, value: str) -> None: + self._description = value + + +class ClientDisconnected(BadRequest): + """Internal exception that is raised if Werkzeug detects a disconnected + client. Since the client is already gone at that point attempting to + send the error message to the client might not work and might ultimately + result in another exception in the server. Mainly this is here so that + it is silenced by default as far as Werkzeug is concerned. + + Since disconnections cannot be reliably detected and are unspecified + by WSGI to a large extent this might or might not be raised if a client + is gone. + + .. versionadded:: 0.8 + """ + + +class SecurityError(BadRequest): + """Raised if something triggers a security error. This is otherwise + exactly like a bad request error. + + .. versionadded:: 0.9 + """ + + +class BadHost(BadRequest): + """Raised if the submitted host is badly formatted. + + .. versionadded:: 0.11.2 + """ + + +class Unauthorized(HTTPException): + """*401* ``Unauthorized`` + + Raise if the user is not authorized to access a resource. + + The ``www_authenticate`` argument should be used to set the + ``WWW-Authenticate`` header. This is used for HTTP basic auth and + other schemes. Use :class:`~werkzeug.datastructures.WWWAuthenticate` + to create correctly formatted values. Strictly speaking a 401 + response is invalid if it doesn't provide at least one value for + this header, although real clients typically don't care. + + :param description: Override the default message used for the body + of the response. + :param www-authenticate: A single value, or list of values, for the + WWW-Authenticate header(s). + + .. versionchanged:: 2.0 + Serialize multiple ``www_authenticate`` items into multiple + ``WWW-Authenticate`` headers, rather than joining them + into a single value, for better interoperability. + + .. versionchanged:: 0.15.3 + If the ``www_authenticate`` argument is not set, the + ``WWW-Authenticate`` header is not set. + + .. versionchanged:: 0.15.3 + The ``response`` argument was restored. + + .. versionchanged:: 0.15.1 + ``description`` was moved back as the first argument, restoring + its previous position. + + .. versionchanged:: 0.15.0 + ``www_authenticate`` was added as the first argument, ahead of + ``description``. + """ + + code = 401 + description = ( + "The server could not verify that you are authorized to access" + " the URL requested. You either supplied the wrong credentials" + " (e.g. a bad password), or your browser doesn't understand" + " how to supply the credentials required." + ) + + def __init__( + self, + description: t.Optional[str] = None, + response: t.Optional["Response"] = None, + www_authenticate: t.Optional[ + t.Union["WWWAuthenticate", t.Iterable["WWWAuthenticate"]] + ] = None, + ) -> None: + super().__init__(description, response) + + from .datastructures import WWWAuthenticate + + if isinstance(www_authenticate, WWWAuthenticate): + www_authenticate = (www_authenticate,) + + self.www_authenticate = www_authenticate + + def get_headers( + self, + environ: t.Optional["WSGIEnvironment"] = None, + scope: t.Optional[dict] = None, + ) -> t.List[t.Tuple[str, str]]: + headers = super().get_headers(environ, scope) + if self.www_authenticate: + headers.extend(("WWW-Authenticate", str(x)) for x in self.www_authenticate) + return headers + + +class Forbidden(HTTPException): + """*403* `Forbidden` + + Raise if the user doesn't have the permission for the requested resource + but was authenticated. + """ + + code = 403 + description = ( + "You don't have the permission to access the requested" + " resource. It is either read-protected or not readable by the" + " server." + ) + + +class NotFound(HTTPException): + """*404* `Not Found` + + Raise if a resource does not exist and never existed. + """ + + code = 404 + description = ( + "The requested URL was not found on the server. If you entered" + " the URL manually please check your spelling and try again." + ) + + +class MethodNotAllowed(HTTPException): + """*405* `Method Not Allowed` + + Raise if the server used a method the resource does not handle. For + example `POST` if the resource is view only. Especially useful for REST. + + The first argument for this exception should be a list of allowed methods. + Strictly speaking the response would be invalid if you don't provide valid + methods in the header which you can do with that list. + """ + + code = 405 + description = "The method is not allowed for the requested URL." + + def __init__( + self, + valid_methods: t.Optional[t.Iterable[str]] = None, + description: t.Optional[str] = None, + response: t.Optional["Response"] = None, + ) -> None: + """Takes an optional list of valid http methods + starting with werkzeug 0.3 the list will be mandatory.""" + super().__init__(description=description, response=response) + self.valid_methods = valid_methods + + def get_headers( + self, + environ: t.Optional["WSGIEnvironment"] = None, + scope: t.Optional[dict] = None, + ) -> t.List[t.Tuple[str, str]]: + headers = super().get_headers(environ, scope) + if self.valid_methods: + headers.append(("Allow", ", ".join(self.valid_methods))) + return headers + + +class NotAcceptable(HTTPException): + """*406* `Not Acceptable` + + Raise if the server can't return any content conforming to the + `Accept` headers of the client. + """ + + code = 406 + description = ( + "The resource identified by the request is only capable of" + " generating response entities which have content" + " characteristics not acceptable according to the accept" + " headers sent in the request." + ) + + +class RequestTimeout(HTTPException): + """*408* `Request Timeout` + + Raise to signalize a timeout. + """ + + code = 408 + description = ( + "The server closed the network connection because the browser" + " didn't finish the request within the specified time." + ) + + +class Conflict(HTTPException): + """*409* `Conflict` + + Raise to signal that a request cannot be completed because it conflicts + with the current state on the server. + + .. versionadded:: 0.7 + """ + + code = 409 + description = ( + "A conflict happened while processing the request. The" + " resource might have been modified while the request was being" + " processed." + ) + + +class Gone(HTTPException): + """*410* `Gone` + + Raise if a resource existed previously and went away without new location. + """ + + code = 410 + description = ( + "The requested URL is no longer available on this server and" + " there is no forwarding address. If you followed a link from a" + " foreign page, please contact the author of this page." + ) + + +class LengthRequired(HTTPException): + """*411* `Length Required` + + Raise if the browser submitted data but no ``Content-Length`` header which + is required for the kind of processing the server does. + """ + + code = 411 + description = ( + "A request with this method requires a valid Content-" + "Length header." + ) + + +class PreconditionFailed(HTTPException): + """*412* `Precondition Failed` + + Status code used in combination with ``If-Match``, ``If-None-Match``, or + ``If-Unmodified-Since``. + """ + + code = 412 + description = ( + "The precondition on the request for the URL failed positive evaluation." + ) + + +class RequestEntityTooLarge(HTTPException): + """*413* `Request Entity Too Large` + + The status code one should return if the data submitted exceeded a given + limit. + """ + + code = 413 + description = "The data value transmitted exceeds the capacity limit." + + +class RequestURITooLarge(HTTPException): + """*414* `Request URI Too Large` + + Like *413* but for too long URLs. + """ + + code = 414 + description = ( + "The length of the requested URL exceeds the capacity limit for" + " this server. The request cannot be processed." + ) + + +class UnsupportedMediaType(HTTPException): + """*415* `Unsupported Media Type` + + The status code returned if the server is unable to handle the media type + the client transmitted. + """ + + code = 415 + description = ( + "The server does not support the media type transmitted in the request." + ) + + +class RequestedRangeNotSatisfiable(HTTPException): + """*416* `Requested Range Not Satisfiable` + + The client asked for an invalid part of the file. + + .. versionadded:: 0.7 + """ + + code = 416 + description = "The server cannot provide the requested range." + + def __init__( + self, + length: t.Optional[int] = None, + units: str = "bytes", + description: t.Optional[str] = None, + response: t.Optional["Response"] = None, + ) -> None: + """Takes an optional `Content-Range` header value based on ``length`` + parameter. + """ + super().__init__(description=description, response=response) + self.length = length + self.units = units + + def get_headers( + self, + environ: t.Optional["WSGIEnvironment"] = None, + scope: t.Optional[dict] = None, + ) -> t.List[t.Tuple[str, str]]: + headers = super().get_headers(environ, scope) + if self.length is not None: + headers.append(("Content-Range", f"{self.units} */{self.length}")) + return headers + + +class ExpectationFailed(HTTPException): + """*417* `Expectation Failed` + + The server cannot meet the requirements of the Expect request-header. + + .. versionadded:: 0.7 + """ + + code = 417 + description = "The server could not meet the requirements of the Expect header" + + +class ImATeapot(HTTPException): + """*418* `I'm a teapot` + + The server should return this if it is a teapot and someone attempted + to brew coffee with it. + + .. versionadded:: 0.7 + """ + + code = 418 + description = "This server is a teapot, not a coffee machine" + + +class UnprocessableEntity(HTTPException): + """*422* `Unprocessable Entity` + + Used if the request is well formed, but the instructions are otherwise + incorrect. + """ + + code = 422 + description = ( + "The request was well-formed but was unable to be followed due" + " to semantic errors." + ) + + +class Locked(HTTPException): + """*423* `Locked` + + Used if the resource that is being accessed is locked. + """ + + code = 423 + description = "The resource that is being accessed is locked." + + +class FailedDependency(HTTPException): + """*424* `Failed Dependency` + + Used if the method could not be performed on the resource + because the requested action depended on another action and that action failed. + """ + + code = 424 + description = ( + "The method could not be performed on the resource because the" + " requested action depended on another action and that action" + " failed." + ) + + +class PreconditionRequired(HTTPException): + """*428* `Precondition Required` + + The server requires this request to be conditional, typically to prevent + the lost update problem, which is a race condition between two or more + clients attempting to update a resource through PUT or DELETE. By requiring + each client to include a conditional header ("If-Match" or "If-Unmodified- + Since") with the proper value retained from a recent GET request, the + server ensures that each client has at least seen the previous revision of + the resource. + """ + + code = 428 + description = ( + "This request is required to be conditional; try using" + ' "If-Match" or "If-Unmodified-Since".' + ) + + +class _RetryAfter(HTTPException): + """Adds an optional ``retry_after`` parameter which will set the + ``Retry-After`` header. May be an :class:`int` number of seconds or + a :class:`~datetime.datetime`. + """ + + def __init__( + self, + description: t.Optional[str] = None, + response: t.Optional["Response"] = None, + retry_after: t.Optional[t.Union[datetime, int]] = None, + ) -> None: + super().__init__(description, response) + self.retry_after = retry_after + + def get_headers( + self, + environ: t.Optional["WSGIEnvironment"] = None, + scope: t.Optional[dict] = None, + ) -> t.List[t.Tuple[str, str]]: + headers = super().get_headers(environ, scope) + + if self.retry_after: + if isinstance(self.retry_after, datetime): + from .http import http_date + + value = http_date(self.retry_after) + else: + value = str(self.retry_after) + + headers.append(("Retry-After", value)) + + return headers + + +class TooManyRequests(_RetryAfter): + """*429* `Too Many Requests` + + The server is limiting the rate at which this user receives + responses, and this request exceeds that rate. (The server may use + any convenient method to identify users and their request rates). + The server may include a "Retry-After" header to indicate how long + the user should wait before retrying. + + :param retry_after: If given, set the ``Retry-After`` header to this + value. May be an :class:`int` number of seconds or a + :class:`~datetime.datetime`. + + .. versionchanged:: 1.0 + Added ``retry_after`` parameter. + """ + + code = 429 + description = "This user has exceeded an allotted request count. Try again later." + + +class RequestHeaderFieldsTooLarge(HTTPException): + """*431* `Request Header Fields Too Large` + + The server refuses to process the request because the header fields are too + large. One or more individual fields may be too large, or the set of all + headers is too large. + """ + + code = 431 + description = "One or more header fields exceeds the maximum size." + + +class UnavailableForLegalReasons(HTTPException): + """*451* `Unavailable For Legal Reasons` + + This status code indicates that the server is denying access to the + resource as a consequence of a legal demand. + """ + + code = 451 + description = "Unavailable for legal reasons." + + +class InternalServerError(HTTPException): + """*500* `Internal Server Error` + + Raise if an internal server error occurred. This is a good fallback if an + unknown error occurred in the dispatcher. + + .. versionchanged:: 1.0.0 + Added the :attr:`original_exception` attribute. + """ + + code = 500 + description = ( + "The server encountered an internal error and was unable to" + " complete your request. Either the server is overloaded or" + " there is an error in the application." + ) + + def __init__( + self, + description: t.Optional[str] = None, + response: t.Optional["Response"] = None, + original_exception: t.Optional[BaseException] = None, + ) -> None: + #: The original exception that caused this 500 error. Can be + #: used by frameworks to provide context when handling + #: unexpected errors. + self.original_exception = original_exception + super().__init__(description=description, response=response) + + +class NotImplemented(HTTPException): + """*501* `Not Implemented` + + Raise if the application does not support the action requested by the + browser. + """ + + code = 501 + description = "The server does not support the action requested by the browser." + + +class BadGateway(HTTPException): + """*502* `Bad Gateway` + + If you do proxying in your application you should return this status code + if you received an invalid response from the upstream server it accessed + in attempting to fulfill the request. + """ + + code = 502 + description = ( + "The proxy server received an invalid response from an upstream server." + ) + + +class ServiceUnavailable(_RetryAfter): + """*503* `Service Unavailable` + + Status code you should return if a service is temporarily + unavailable. + + :param retry_after: If given, set the ``Retry-After`` header to this + value. May be an :class:`int` number of seconds or a + :class:`~datetime.datetime`. + + .. versionchanged:: 1.0 + Added ``retry_after`` parameter. + """ + + code = 503 + description = ( + "The server is temporarily unable to service your request due" + " to maintenance downtime or capacity problems. Please try" + " again later." + ) + + +class GatewayTimeout(HTTPException): + """*504* `Gateway Timeout` + + Status code you should return if a connection to an upstream server + times out. + """ + + code = 504 + description = "The connection to an upstream server timed out." + + +class HTTPVersionNotSupported(HTTPException): + """*505* `HTTP Version Not Supported` + + The server does not support the HTTP protocol version used in the request. + """ + + code = 505 + description = ( + "The server does not support the HTTP protocol version used in the request." + ) + + +default_exceptions: t.Dict[int, t.Type[HTTPException]] = {} + + +def _find_exceptions() -> None: + for obj in globals().values(): + try: + is_http_exception = issubclass(obj, HTTPException) + except TypeError: + is_http_exception = False + if not is_http_exception or obj.code is None: + continue + old_obj = default_exceptions.get(obj.code, None) + if old_obj is not None and issubclass(obj, old_obj): + continue + default_exceptions[obj.code] = obj + + +_find_exceptions() +del _find_exceptions + + +class Aborter: + """When passed a dict of code -> exception items it can be used as + callable that raises exceptions. If the first argument to the + callable is an integer it will be looked up in the mapping, if it's + a WSGI application it will be raised in a proxy exception. + + The rest of the arguments are forwarded to the exception constructor. + """ + + def __init__( + self, + mapping: t.Optional[t.Dict[int, t.Type[HTTPException]]] = None, + extra: t.Optional[t.Dict[int, t.Type[HTTPException]]] = None, + ) -> None: + if mapping is None: + mapping = default_exceptions + self.mapping = dict(mapping) + if extra is not None: + self.mapping.update(extra) + + def __call__( + self, code: t.Union[int, "Response"], *args: t.Any, **kwargs: t.Any + ) -> "te.NoReturn": + from .sansio.response import Response + + if isinstance(code, Response): + raise HTTPException(response=code) + + if code not in self.mapping: + raise LookupError(f"no exception for {code!r}") + + raise self.mapping[code](*args, **kwargs) + + +def abort( + status: t.Union[int, "Response"], *args: t.Any, **kwargs: t.Any +) -> "te.NoReturn": + """Raises an :py:exc:`HTTPException` for the given status code or WSGI + application. + + If a status code is given, it will be looked up in the list of + exceptions and will raise that exception. If passed a WSGI application, + it will wrap it in a proxy WSGI exception and raise that:: + + abort(404) # 404 Not Found + abort(Response('Hello World')) + + """ + _aborter(status, *args, **kwargs) + + +_aborter: Aborter = Aborter() diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/filesystem.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/filesystem.py new file mode 100644 index 000000000..36a3d12e9 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/filesystem.py @@ -0,0 +1,55 @@ +import codecs +import sys +import typing as t +import warnings + +# We do not trust traditional unixes. +has_likely_buggy_unicode_filesystem = ( + sys.platform.startswith("linux") or "bsd" in sys.platform +) + + +def _is_ascii_encoding(encoding: t.Optional[str]) -> bool: + """Given an encoding this figures out if the encoding is actually ASCII (which + is something we don't actually want in most cases). This is necessary + because ASCII comes under many names such as ANSI_X3.4-1968. + """ + if encoding is None: + return False + try: + return codecs.lookup(encoding).name == "ascii" + except LookupError: + return False + + +class BrokenFilesystemWarning(RuntimeWarning, UnicodeWarning): + """The warning used by Werkzeug to signal a broken filesystem. Will only be + used once per runtime.""" + + +_warned_about_filesystem_encoding = False + + +def get_filesystem_encoding() -> str: + """Returns the filesystem encoding that should be used. Note that this is + different from the Python understanding of the filesystem encoding which + might be deeply flawed. Do not use this value against Python's string APIs + because it might be different. See :ref:`filesystem-encoding` for the exact + behavior. + + The concept of a filesystem encoding in generally is not something you + should rely on. As such if you ever need to use this function except for + writing wrapper code reconsider. + """ + global _warned_about_filesystem_encoding + rv = sys.getfilesystemencoding() + if has_likely_buggy_unicode_filesystem and not rv or _is_ascii_encoding(rv): + if not _warned_about_filesystem_encoding: + warnings.warn( + "Detected a misconfigured UNIX filesystem: Will use" + f" UTF-8 as filesystem encoding instead of {rv!r}", + BrokenFilesystemWarning, + ) + _warned_about_filesystem_encoding = True + return "utf-8" + return rv diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/formparser.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/formparser.py new file mode 100644 index 000000000..6cb758feb --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/formparser.py @@ -0,0 +1,495 @@ +import typing as t +import warnings +from functools import update_wrapper +from io import BytesIO +from itertools import chain +from typing import Union + +from . import exceptions +from ._internal import _to_str +from .datastructures import FileStorage +from .datastructures import Headers +from .datastructures import MultiDict +from .http import parse_options_header +from .sansio.multipart import Data +from .sansio.multipart import Epilogue +from .sansio.multipart import Field +from .sansio.multipart import File +from .sansio.multipart import MultipartDecoder +from .sansio.multipart import NeedData +from .urls import url_decode_stream +from .wsgi import _make_chunk_iter +from .wsgi import get_content_length +from .wsgi import get_input_stream + +# there are some platforms where SpooledTemporaryFile is not available. +# In that case we need to provide a fallback. +try: + from tempfile import SpooledTemporaryFile +except ImportError: + from tempfile import TemporaryFile + + SpooledTemporaryFile = None # type: ignore + +if t.TYPE_CHECKING: + import typing as te + from _typeshed.wsgi import WSGIEnvironment + + t_parse_result = t.Tuple[t.IO[bytes], MultiDict, MultiDict] + + class TStreamFactory(te.Protocol): + def __call__( + self, + total_content_length: t.Optional[int], + content_type: t.Optional[str], + filename: t.Optional[str], + content_length: t.Optional[int] = None, + ) -> t.IO[bytes]: + ... + + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + + +def _exhaust(stream: t.IO[bytes]) -> None: + bts = stream.read(64 * 1024) + while bts: + bts = stream.read(64 * 1024) + + +def default_stream_factory( + total_content_length: t.Optional[int], + content_type: t.Optional[str], + filename: t.Optional[str], + content_length: t.Optional[int] = None, +) -> t.IO[bytes]: + max_size = 1024 * 500 + + if SpooledTemporaryFile is not None: + return t.cast(t.IO[bytes], SpooledTemporaryFile(max_size=max_size, mode="rb+")) + elif total_content_length is None or total_content_length > max_size: + return t.cast(t.IO[bytes], TemporaryFile("rb+")) + + return BytesIO() + + +def parse_form_data( + environ: "WSGIEnvironment", + stream_factory: t.Optional["TStreamFactory"] = None, + charset: str = "utf-8", + errors: str = "replace", + max_form_memory_size: t.Optional[int] = None, + max_content_length: t.Optional[int] = None, + cls: t.Optional[t.Type[MultiDict]] = None, + silent: bool = True, +) -> "t_parse_result": + """Parse the form data in the environ and return it as tuple in the form + ``(stream, form, files)``. You should only call this method if the + transport method is `POST`, `PUT`, or `PATCH`. + + If the mimetype of the data transmitted is `multipart/form-data` the + files multidict will be filled with `FileStorage` objects. If the + mimetype is unknown the input stream is wrapped and returned as first + argument, else the stream is empty. + + This is a shortcut for the common usage of :class:`FormDataParser`. + + Have a look at :doc:`/request_data` for more details. + + .. versionadded:: 0.5 + The `max_form_memory_size`, `max_content_length` and + `cls` parameters were added. + + .. versionadded:: 0.5.1 + The optional `silent` flag was added. + + :param environ: the WSGI environment to be used for parsing. + :param stream_factory: An optional callable that returns a new read and + writeable file descriptor. This callable works + the same as :meth:`Response._get_file_stream`. + :param charset: The character set for URL and url encoded form data. + :param errors: The encoding error behavior. + :param max_form_memory_size: the maximum number of bytes to be accepted for + in-memory stored form data. If the data + exceeds the value specified an + :exc:`~exceptions.RequestEntityTooLarge` + exception is raised. + :param max_content_length: If this is provided and the transmitted data + is longer than this value an + :exc:`~exceptions.RequestEntityTooLarge` + exception is raised. + :param cls: an optional dict class to use. If this is not specified + or `None` the default :class:`MultiDict` is used. + :param silent: If set to False parsing errors will not be caught. + :return: A tuple in the form ``(stream, form, files)``. + """ + return FormDataParser( + stream_factory, + charset, + errors, + max_form_memory_size, + max_content_length, + cls, + silent, + ).parse_from_environ(environ) + + +def exhaust_stream(f: F) -> F: + """Helper decorator for methods that exhausts the stream on return.""" + + def wrapper(self, stream, *args, **kwargs): # type: ignore + try: + return f(self, stream, *args, **kwargs) + finally: + exhaust = getattr(stream, "exhaust", None) + + if exhaust is not None: + exhaust() + else: + while True: + chunk = stream.read(1024 * 64) + + if not chunk: + break + + return update_wrapper(t.cast(F, wrapper), f) + + +class FormDataParser: + """This class implements parsing of form data for Werkzeug. By itself + it can parse multipart and url encoded form data. It can be subclassed + and extended but for most mimetypes it is a better idea to use the + untouched stream and expose it as separate attributes on a request + object. + + .. versionadded:: 0.8 + + :param stream_factory: An optional callable that returns a new read and + writeable file descriptor. This callable works + the same as :meth:`Response._get_file_stream`. + :param charset: The character set for URL and url encoded form data. + :param errors: The encoding error behavior. + :param max_form_memory_size: the maximum number of bytes to be accepted for + in-memory stored form data. If the data + exceeds the value specified an + :exc:`~exceptions.RequestEntityTooLarge` + exception is raised. + :param max_content_length: If this is provided and the transmitted data + is longer than this value an + :exc:`~exceptions.RequestEntityTooLarge` + exception is raised. + :param cls: an optional dict class to use. If this is not specified + or `None` the default :class:`MultiDict` is used. + :param silent: If set to False parsing errors will not be caught. + """ + + def __init__( + self, + stream_factory: t.Optional["TStreamFactory"] = None, + charset: str = "utf-8", + errors: str = "replace", + max_form_memory_size: t.Optional[int] = None, + max_content_length: t.Optional[int] = None, + cls: t.Optional[t.Type[MultiDict]] = None, + silent: bool = True, + ) -> None: + if stream_factory is None: + stream_factory = default_stream_factory + + self.stream_factory = stream_factory + self.charset = charset + self.errors = errors + self.max_form_memory_size = max_form_memory_size + self.max_content_length = max_content_length + + if cls is None: + cls = MultiDict + + self.cls = cls + self.silent = silent + + def get_parse_func( + self, mimetype: str, options: t.Dict[str, str] + ) -> t.Optional[ + t.Callable[ + ["FormDataParser", t.IO[bytes], str, t.Optional[int], t.Dict[str, str]], + "t_parse_result", + ] + ]: + return self.parse_functions.get(mimetype) + + def parse_from_environ(self, environ: "WSGIEnvironment") -> "t_parse_result": + """Parses the information from the environment as form data. + + :param environ: the WSGI environment to be used for parsing. + :return: A tuple in the form ``(stream, form, files)``. + """ + content_type = environ.get("CONTENT_TYPE", "") + content_length = get_content_length(environ) + mimetype, options = parse_options_header(content_type) + return self.parse(get_input_stream(environ), mimetype, content_length, options) + + def parse( + self, + stream: t.IO[bytes], + mimetype: str, + content_length: t.Optional[int], + options: t.Optional[t.Dict[str, str]] = None, + ) -> "t_parse_result": + """Parses the information from the given stream, mimetype, + content length and mimetype parameters. + + :param stream: an input stream + :param mimetype: the mimetype of the data + :param content_length: the content length of the incoming data + :param options: optional mimetype parameters (used for + the multipart boundary for instance) + :return: A tuple in the form ``(stream, form, files)``. + """ + if ( + self.max_content_length is not None + and content_length is not None + and content_length > self.max_content_length + ): + # if the input stream is not exhausted, firefox reports Connection Reset + _exhaust(stream) + raise exceptions.RequestEntityTooLarge() + + if options is None: + options = {} + + parse_func = self.get_parse_func(mimetype, options) + + if parse_func is not None: + try: + return parse_func(self, stream, mimetype, content_length, options) + except ValueError: + if not self.silent: + raise + + return stream, self.cls(), self.cls() + + @exhaust_stream + def _parse_multipart( + self, + stream: t.IO[bytes], + mimetype: str, + content_length: t.Optional[int], + options: t.Dict[str, str], + ) -> "t_parse_result": + parser = MultiPartParser( + self.stream_factory, + self.charset, + self.errors, + max_form_memory_size=self.max_form_memory_size, + cls=self.cls, + ) + boundary = options.get("boundary", "").encode("ascii") + + if not boundary: + raise ValueError("Missing boundary") + + form, files = parser.parse(stream, boundary, content_length) + return stream, form, files + + @exhaust_stream + def _parse_urlencoded( + self, + stream: t.IO[bytes], + mimetype: str, + content_length: t.Optional[int], + options: t.Dict[str, str], + ) -> "t_parse_result": + if ( + self.max_form_memory_size is not None + and content_length is not None + and content_length > self.max_form_memory_size + ): + # if the input stream is not exhausted, firefox reports Connection Reset + _exhaust(stream) + raise exceptions.RequestEntityTooLarge() + + form = url_decode_stream(stream, self.charset, errors=self.errors, cls=self.cls) + return stream, form, self.cls() + + #: mapping of mimetypes to parsing functions + parse_functions: t.Dict[ + str, + t.Callable[ + ["FormDataParser", t.IO[bytes], str, t.Optional[int], t.Dict[str, str]], + "t_parse_result", + ], + ] = { + "multipart/form-data": _parse_multipart, + "application/x-www-form-urlencoded": _parse_urlencoded, + "application/x-url-encoded": _parse_urlencoded, + } + + +def _line_parse(line: str) -> t.Tuple[str, bool]: + """Removes line ending characters and returns a tuple (`stripped_line`, + `is_terminated`). + """ + if line[-2:] == "\r\n": + return line[:-2], True + + elif line[-1:] in {"\r", "\n"}: + return line[:-1], True + + return line, False + + +def parse_multipart_headers(iterable: t.Iterable[bytes]) -> Headers: + """Parses multipart headers from an iterable that yields lines (including + the trailing newline symbol). The iterable has to be newline terminated. + The iterable will stop at the line where the headers ended so it can be + further consumed. + :param iterable: iterable of strings that are newline terminated + """ + warnings.warn( + "'parse_multipart_headers' is deprecated and will be removed in" + " Werkzeug 2.1.", + DeprecationWarning, + stacklevel=2, + ) + result: t.List[t.Tuple[str, str]] = [] + + for b_line in iterable: + line = _to_str(b_line) + line, line_terminated = _line_parse(line) + + if not line_terminated: + raise ValueError("unexpected end of line in multipart header") + + if not line: + break + elif line[0] in " \t" and result: + key, value = result[-1] + result[-1] = (key, f"{value}\n {line[1:]}") + else: + parts = line.split(":", 1) + + if len(parts) == 2: + result.append((parts[0].strip(), parts[1].strip())) + + # we link the list to the headers, no need to create a copy, the + # list was not shared anyways. + return Headers(result) + + +class MultiPartParser: + def __init__( + self, + stream_factory: t.Optional["TStreamFactory"] = None, + charset: str = "utf-8", + errors: str = "replace", + max_form_memory_size: t.Optional[int] = None, + cls: t.Optional[t.Type[MultiDict]] = None, + buffer_size: int = 64 * 1024, + ) -> None: + self.charset = charset + self.errors = errors + self.max_form_memory_size = max_form_memory_size + + if stream_factory is None: + stream_factory = default_stream_factory + + self.stream_factory = stream_factory + + if cls is None: + cls = MultiDict + + self.cls = cls + + self.buffer_size = buffer_size + + def fail(self, message: str) -> "te.NoReturn": + raise ValueError(message) + + def get_part_charset(self, headers: Headers) -> str: + # Figure out input charset for current part + content_type = headers.get("content-type") + + if content_type: + mimetype, ct_params = parse_options_header(content_type) + return ct_params.get("charset", self.charset) + + return self.charset + + def start_file_streaming( + self, event: File, total_content_length: t.Optional[int] + ) -> t.IO[bytes]: + content_type = event.headers.get("content-type") + + try: + content_length = int(event.headers["content-length"]) + except (KeyError, ValueError): + content_length = 0 + + container = self.stream_factory( + total_content_length=total_content_length, + filename=event.filename, + content_type=content_type, + content_length=content_length, + ) + return container + + def parse( + self, stream: t.IO[bytes], boundary: bytes, content_length: t.Optional[int] + ) -> t.Tuple[MultiDict, MultiDict]: + container: t.Union[t.IO[bytes], t.List[bytes]] + _write: t.Callable[[bytes], t.Any] + + iterator = chain( + _make_chunk_iter( + stream, + limit=content_length, + buffer_size=self.buffer_size, + ), + [None], + ) + + parser = MultipartDecoder(boundary, self.max_form_memory_size) + + fields = [] + files = [] + + current_part: Union[Field, File] + for data in iterator: + parser.receive_data(data) + event = parser.next_event() + while not isinstance(event, (Epilogue, NeedData)): + if isinstance(event, Field): + current_part = event + container = [] + _write = container.append + elif isinstance(event, File): + current_part = event + container = self.start_file_streaming(event, content_length) + _write = container.write + elif isinstance(event, Data): + _write(event.data) + if not event.more_data: + if isinstance(current_part, Field): + value = b"".join(container).decode( + self.get_part_charset(current_part.headers), self.errors + ) + fields.append((current_part.name, value)) + else: + container = t.cast(t.IO[bytes], container) + container.seek(0) + files.append( + ( + current_part.name, + FileStorage( + container, + current_part.filename, + current_part.name, + headers=current_part.headers, + ), + ) + ) + + event = parser.next_event() + + return self.cls(fields), self.cls(files) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/http.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/http.py new file mode 100644 index 000000000..ca48fe215 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/http.py @@ -0,0 +1,1388 @@ +import base64 +import email.utils +import re +import typing +import typing as t +import warnings +from datetime import date +from datetime import datetime +from datetime import time +from datetime import timedelta +from datetime import timezone +from enum import Enum +from hashlib import sha1 +from time import mktime +from time import struct_time +from urllib.parse import unquote_to_bytes as _unquote +from urllib.request import parse_http_list as _parse_list_header + +from ._internal import _cookie_parse_impl +from ._internal import _cookie_quote +from ._internal import _make_cookie_domain +from ._internal import _to_bytes +from ._internal import _to_str +from ._internal import _wsgi_decoding_dance +from werkzeug._internal import _dt_as_utc + +if t.TYPE_CHECKING: + import typing_extensions as te + from _typeshed.wsgi import WSGIEnvironment + +# for explanation of "media-range", etc. see Sections 5.3.{1,2} of RFC 7231 +_accept_re = re.compile( + r""" + ( # media-range capturing-parenthesis + [^\s;,]+ # type/subtype + (?:[ \t]*;[ \t]* # ";" + (?: # parameter non-capturing-parenthesis + [^\s;,q][^\s;,]* # token that doesn't start with "q" + | # or + q[^\s;,=][^\s;,]* # token that is more than just "q" + ) + )* # zero or more parameters + ) # end of media-range + (?:[ \t]*;[ \t]*q= # weight is a "q" parameter + (\d*(?:\.\d+)?) # qvalue capturing-parentheses + [^,]* # "extension" accept params: who cares? + )? # accept params are optional + """, + re.VERBOSE, +) +_token_chars = frozenset( + "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~" +) +_etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)') +_option_header_piece_re = re.compile( + r""" + ;\s*,?\s* # newlines were replaced with commas + (?P + "[^"\\]*(?:\\.[^"\\]*)*" # quoted string + | + [^\s;,=*]+ # token + ) + (?:\*(?P\d+))? # *1, optional continuation index + \s* + (?: # optionally followed by =value + (?: # equals sign, possibly with encoding + \*\s*=\s* # * indicates extended notation + (?: # optional encoding + (?P[^\s]+?) + '(?P[^\s]*?)' + )? + | + =\s* # basic notation + ) + (?P + "[^"\\]*(?:\\.[^"\\]*)*" # quoted string + | + [^;,]+ # token + )? + )? + \s* + """, + flags=re.VERBOSE, +) +_option_header_start_mime_type = re.compile(r",\s*([^;,\s]+)([;,]\s*.+)?") +_entity_headers = frozenset( + [ + "allow", + "content-encoding", + "content-language", + "content-length", + "content-location", + "content-md5", + "content-range", + "content-type", + "expires", + "last-modified", + ] +) +_hop_by_hop_headers = frozenset( + [ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", + ] +) +HTTP_STATUS_CODES = { + 100: "Continue", + 101: "Switching Protocols", + 102: "Processing", + 103: "Early Hints", # see RFC 8297 + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 207: "Multi Status", + 208: "Already Reported", # see RFC 5842 + 226: "IM Used", # see RFC 3229 + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 306: "Switch Proxy", # unused + 307: "Temporary Redirect", + 308: "Permanent Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", # unused + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 418: "I'm a teapot", # see RFC 2324 + 421: "Misdirected Request", # see RFC 7540 + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 425: "Too Early", # see RFC 8470 + 426: "Upgrade Required", + 428: "Precondition Required", # see RFC 6585 + 429: "Too Many Requests", + 431: "Request Header Fields Too Large", + 449: "Retry With", # proprietary MS extension + 451: "Unavailable For Legal Reasons", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", # see RFC 2295 + 507: "Insufficient Storage", + 508: "Loop Detected", # see RFC 5842 + 510: "Not Extended", + 511: "Network Authentication Failed", +} + + +class COEP(Enum): + """Cross Origin Embedder Policies""" + + UNSAFE_NONE = "unsafe-none" + REQUIRE_CORP = "require-corp" + + +class COOP(Enum): + """Cross Origin Opener Policies""" + + UNSAFE_NONE = "unsafe-none" + SAME_ORIGIN_ALLOW_POPUPS = "same-origin-allow-popups" + SAME_ORIGIN = "same-origin" + + +def quote_header_value( + value: t.Union[str, int], extra_chars: str = "", allow_token: bool = True +) -> str: + """Quote a header value if necessary. + + .. versionadded:: 0.5 + + :param value: the value to quote. + :param extra_chars: a list of extra characters to skip quoting. + :param allow_token: if this is enabled token values are returned + unchanged. + """ + if isinstance(value, bytes): + value = value.decode("latin1") + value = str(value) + if allow_token: + token_chars = _token_chars | set(extra_chars) + if set(value).issubset(token_chars): + return value + value = value.replace("\\", "\\\\").replace('"', '\\"') + return f'"{value}"' + + +def unquote_header_value(value: str, is_filename: bool = False) -> str: + r"""Unquotes a header value. (Reversal of :func:`quote_header_value`). + This does not use the real unquoting but what browsers are actually + using for quoting. + + .. versionadded:: 0.5 + + :param value: the header value to unquote. + :param is_filename: The value represents a filename or path. + """ + if value and value[0] == value[-1] == '"': + # this is not the real unquoting, but fixing this so that the + # RFC is met will result in bugs with internet explorer and + # probably some other browsers as well. IE for example is + # uploading files with "C:\foo\bar.txt" as filename + value = value[1:-1] + + # if this is a filename and the starting characters look like + # a UNC path, then just return the value without quotes. Using the + # replace sequence below on a UNC path has the effect of turning + # the leading double slash into a single slash and then + # _fix_ie_filename() doesn't work correctly. See #458. + if not is_filename or value[:2] != "\\\\": + return value.replace("\\\\", "\\").replace('\\"', '"') + return value + + +def dump_options_header( + header: t.Optional[str], options: t.Mapping[str, t.Optional[t.Union[str, int]]] +) -> str: + """The reverse function to :func:`parse_options_header`. + + :param header: the header to dump + :param options: a dict of options to append. + """ + segments = [] + if header is not None: + segments.append(header) + for key, value in options.items(): + if value is None: + segments.append(key) + else: + segments.append(f"{key}={quote_header_value(value)}") + return "; ".join(segments) + + +def dump_header( + iterable: t.Union[t.Dict[str, t.Union[str, int]], t.Iterable[str]], + allow_token: bool = True, +) -> str: + """Dump an HTTP header again. This is the reversal of + :func:`parse_list_header`, :func:`parse_set_header` and + :func:`parse_dict_header`. This also quotes strings that include an + equals sign unless you pass it as dict of key, value pairs. + + >>> dump_header({'foo': 'bar baz'}) + 'foo="bar baz"' + >>> dump_header(('foo', 'bar baz')) + 'foo, "bar baz"' + + :param iterable: the iterable or dict of values to quote. + :param allow_token: if set to `False` tokens as values are disallowed. + See :func:`quote_header_value` for more details. + """ + if isinstance(iterable, dict): + items = [] + for key, value in iterable.items(): + if value is None: + items.append(key) + else: + items.append( + f"{key}={quote_header_value(value, allow_token=allow_token)}" + ) + else: + items = [quote_header_value(x, allow_token=allow_token) for x in iterable] + return ", ".join(items) + + +def dump_csp_header(header: "ds.ContentSecurityPolicy") -> str: + """Dump a Content Security Policy header. + + These are structured into policies such as "default-src 'self'; + script-src 'self'". + + .. versionadded:: 1.0.0 + Support for Content Security Policy headers was added. + + """ + return "; ".join(f"{key} {value}" for key, value in header.items()) + + +def parse_list_header(value: str) -> t.List[str]: + """Parse lists as described by RFC 2068 Section 2. + + In particular, parse comma-separated lists where the elements of + the list may include quoted-strings. A quoted-string could + contain a comma. A non-quoted string could have quotes in the + middle. Quotes are removed automatically after parsing. + + It basically works like :func:`parse_set_header` just that items + may appear multiple times and case sensitivity is preserved. + + The return value is a standard :class:`list`: + + >>> parse_list_header('token, "quoted value"') + ['token', 'quoted value'] + + To create a header from the :class:`list` again, use the + :func:`dump_header` function. + + :param value: a string with a list header. + :return: :class:`list` + """ + result = [] + for item in _parse_list_header(value): + if item[:1] == item[-1:] == '"': + item = unquote_header_value(item[1:-1]) + result.append(item) + return result + + +def parse_dict_header(value: str, cls: t.Type[dict] = dict) -> t.Dict[str, str]: + """Parse lists of key, value pairs as described by RFC 2068 Section 2 and + convert them into a python dict (or any other mapping object created from + the type with a dict like interface provided by the `cls` argument): + + >>> d = parse_dict_header('foo="is a fish", bar="as well"') + >>> type(d) is dict + True + >>> sorted(d.items()) + [('bar', 'as well'), ('foo', 'is a fish')] + + If there is no value for a key it will be `None`: + + >>> parse_dict_header('key_without_value') + {'key_without_value': None} + + To create a header from the :class:`dict` again, use the + :func:`dump_header` function. + + .. versionchanged:: 0.9 + Added support for `cls` argument. + + :param value: a string with a dict header. + :param cls: callable to use for storage of parsed results. + :return: an instance of `cls` + """ + result = cls() + if isinstance(value, bytes): + value = value.decode("latin1") + for item in _parse_list_header(value): + if "=" not in item: + result[item] = None + continue + name, value = item.split("=", 1) + if value[:1] == value[-1:] == '"': + value = unquote_header_value(value[1:-1]) + result[name] = value + return result + + +@typing.overload +def parse_options_header( + value: t.Optional[str], multiple: "te.Literal[False]" = False +) -> t.Tuple[str, t.Dict[str, str]]: + ... + + +@typing.overload +def parse_options_header( + value: t.Optional[str], multiple: "te.Literal[True]" +) -> t.Tuple[t.Any, ...]: + ... + + +def parse_options_header( + value: t.Optional[str], multiple: bool = False +) -> t.Union[t.Tuple[str, t.Dict[str, str]], t.Tuple[t.Any, ...]]: + """Parse a ``Content-Type`` like header into a tuple with the content + type and the options: + + >>> parse_options_header('text/html; charset=utf8') + ('text/html', {'charset': 'utf8'}) + + This should not be used to parse ``Cache-Control`` like headers that use + a slightly different format. For these headers use the + :func:`parse_dict_header` function. + + .. versionchanged:: 0.15 + :rfc:`2231` parameter continuations are handled. + + .. versionadded:: 0.5 + + :param value: the header to parse. + :param multiple: Whether try to parse and return multiple MIME types + :return: (mimetype, options) or (mimetype, options, mimetype, options, …) + if multiple=True + """ + if not value: + return "", {} + + result: t.List[t.Any] = [] + + value = "," + value.replace("\n", ",") + while value: + match = _option_header_start_mime_type.match(value) + if not match: + break + result.append(match.group(1)) # mimetype + options: t.Dict[str, str] = {} + # Parse options + rest = match.group(2) + encoding: t.Optional[str] + continued_encoding: t.Optional[str] = None + while rest: + optmatch = _option_header_piece_re.match(rest) + if not optmatch: + break + option, count, encoding, language, option_value = optmatch.groups() + # Continuations don't have to supply the encoding after the + # first line. If we're in a continuation, track the current + # encoding to use for subsequent lines. Reset it when the + # continuation ends. + if not count: + continued_encoding = None + else: + if not encoding: + encoding = continued_encoding + continued_encoding = encoding + option = unquote_header_value(option) + if option_value is not None: + option_value = unquote_header_value(option_value, option == "filename") + if encoding is not None: + option_value = _unquote(option_value).decode(encoding) + if count: + # Continuations append to the existing value. For + # simplicity, this ignores the possibility of + # out-of-order indices, which shouldn't happen anyway. + options[option] = options.get(option, "") + option_value + else: + options[option] = option_value + rest = rest[optmatch.end() :] + result.append(options) + if multiple is False: + return tuple(result) + value = rest + + return tuple(result) if result else ("", {}) + + +_TAnyAccept = t.TypeVar("_TAnyAccept", bound="ds.Accept") + + +@typing.overload +def parse_accept_header(value: t.Optional[str]) -> "ds.Accept": + ... + + +@typing.overload +def parse_accept_header( + value: t.Optional[str], cls: t.Type[_TAnyAccept] +) -> _TAnyAccept: + ... + + +def parse_accept_header( + value: t.Optional[str], cls: t.Optional[t.Type[_TAnyAccept]] = None +) -> _TAnyAccept: + """Parses an HTTP Accept-* header. This does not implement a complete + valid algorithm but one that supports at least value and quality + extraction. + + Returns a new :class:`Accept` object (basically a list of ``(value, quality)`` + tuples sorted by the quality with some additional accessor methods). + + The second parameter can be a subclass of :class:`Accept` that is created + with the parsed values and returned. + + :param value: the accept header string to be parsed. + :param cls: the wrapper class for the return value (can be + :class:`Accept` or a subclass thereof) + :return: an instance of `cls`. + """ + if cls is None: + cls = t.cast(t.Type[_TAnyAccept], ds.Accept) + + if not value: + return cls(None) + + result = [] + for match in _accept_re.finditer(value): + quality_match = match.group(2) + if not quality_match: + quality: float = 1 + else: + quality = max(min(float(quality_match), 1), 0) + result.append((match.group(1), quality)) + return cls(result) + + +_TAnyCC = t.TypeVar("_TAnyCC", bound="ds._CacheControl") +_t_cc_update = t.Optional[t.Callable[[_TAnyCC], None]] + + +@typing.overload +def parse_cache_control_header( + value: t.Optional[str], on_update: _t_cc_update, cls: None = None +) -> "ds.RequestCacheControl": + ... + + +@typing.overload +def parse_cache_control_header( + value: t.Optional[str], on_update: _t_cc_update, cls: t.Type[_TAnyCC] +) -> _TAnyCC: + ... + + +def parse_cache_control_header( + value: t.Optional[str], + on_update: _t_cc_update = None, + cls: t.Optional[t.Type[_TAnyCC]] = None, +) -> _TAnyCC: + """Parse a cache control header. The RFC differs between response and + request cache control, this method does not. It's your responsibility + to not use the wrong control statements. + + .. versionadded:: 0.5 + The `cls` was added. If not specified an immutable + :class:`~werkzeug.datastructures.RequestCacheControl` is returned. + + :param value: a cache control header to be parsed. + :param on_update: an optional callable that is called every time a value + on the :class:`~werkzeug.datastructures.CacheControl` + object is changed. + :param cls: the class for the returned object. By default + :class:`~werkzeug.datastructures.RequestCacheControl` is used. + :return: a `cls` object. + """ + if cls is None: + cls = t.cast(t.Type[_TAnyCC], ds.RequestCacheControl) + + if not value: + return cls((), on_update) + + return cls(parse_dict_header(value), on_update) + + +_TAnyCSP = t.TypeVar("_TAnyCSP", bound="ds.ContentSecurityPolicy") +_t_csp_update = t.Optional[t.Callable[[_TAnyCSP], None]] + + +@typing.overload +def parse_csp_header( + value: t.Optional[str], on_update: _t_csp_update, cls: None = None +) -> "ds.ContentSecurityPolicy": + ... + + +@typing.overload +def parse_csp_header( + value: t.Optional[str], on_update: _t_csp_update, cls: t.Type[_TAnyCSP] +) -> _TAnyCSP: + ... + + +def parse_csp_header( + value: t.Optional[str], + on_update: _t_csp_update = None, + cls: t.Optional[t.Type[_TAnyCSP]] = None, +) -> _TAnyCSP: + """Parse a Content Security Policy header. + + .. versionadded:: 1.0.0 + Support for Content Security Policy headers was added. + + :param value: a csp header to be parsed. + :param on_update: an optional callable that is called every time a value + on the object is changed. + :param cls: the class for the returned object. By default + :class:`~werkzeug.datastructures.ContentSecurityPolicy` is used. + :return: a `cls` object. + """ + if cls is None: + cls = t.cast(t.Type[_TAnyCSP], ds.ContentSecurityPolicy) + + if value is None: + return cls((), on_update) + + items = [] + + for policy in value.split(";"): + policy = policy.strip() + + # Ignore badly formatted policies (no space) + if " " in policy: + directive, value = policy.strip().split(" ", 1) + items.append((directive.strip(), value.strip())) + + return cls(items, on_update) + + +def parse_set_header( + value: t.Optional[str], + on_update: t.Optional[t.Callable[["ds.HeaderSet"], None]] = None, +) -> "ds.HeaderSet": + """Parse a set-like header and return a + :class:`~werkzeug.datastructures.HeaderSet` object: + + >>> hs = parse_set_header('token, "quoted value"') + + The return value is an object that treats the items case-insensitively + and keeps the order of the items: + + >>> 'TOKEN' in hs + True + >>> hs.index('quoted value') + 1 + >>> hs + HeaderSet(['token', 'quoted value']) + + To create a header from the :class:`HeaderSet` again, use the + :func:`dump_header` function. + + :param value: a set header to be parsed. + :param on_update: an optional callable that is called every time a + value on the :class:`~werkzeug.datastructures.HeaderSet` + object is changed. + :return: a :class:`~werkzeug.datastructures.HeaderSet` + """ + if not value: + return ds.HeaderSet(None, on_update) + return ds.HeaderSet(parse_list_header(value), on_update) + + +def parse_authorization_header( + value: t.Optional[str], +) -> t.Optional["ds.Authorization"]: + """Parse an HTTP basic/digest authorization header transmitted by the web + browser. The return value is either `None` if the header was invalid or + not given, otherwise an :class:`~werkzeug.datastructures.Authorization` + object. + + :param value: the authorization header to parse. + :return: a :class:`~werkzeug.datastructures.Authorization` object or `None`. + """ + if not value: + return None + value = _wsgi_decoding_dance(value) + try: + auth_type, auth_info = value.split(None, 1) + auth_type = auth_type.lower() + except ValueError: + return None + if auth_type == "basic": + try: + username, password = base64.b64decode(auth_info).split(b":", 1) + except Exception: + return None + try: + return ds.Authorization( + "basic", + { + "username": _to_str(username, "utf-8"), + "password": _to_str(password, "utf-8"), + }, + ) + except UnicodeDecodeError: + return None + elif auth_type == "digest": + auth_map = parse_dict_header(auth_info) + for key in "username", "realm", "nonce", "uri", "response": + if key not in auth_map: + return None + if "qop" in auth_map: + if not auth_map.get("nc") or not auth_map.get("cnonce"): + return None + return ds.Authorization("digest", auth_map) + return None + + +def parse_www_authenticate_header( + value: t.Optional[str], + on_update: t.Optional[t.Callable[["ds.WWWAuthenticate"], None]] = None, +) -> "ds.WWWAuthenticate": + """Parse an HTTP WWW-Authenticate header into a + :class:`~werkzeug.datastructures.WWWAuthenticate` object. + + :param value: a WWW-Authenticate header to parse. + :param on_update: an optional callable that is called every time a value + on the :class:`~werkzeug.datastructures.WWWAuthenticate` + object is changed. + :return: a :class:`~werkzeug.datastructures.WWWAuthenticate` object. + """ + if not value: + return ds.WWWAuthenticate(on_update=on_update) + try: + auth_type, auth_info = value.split(None, 1) + auth_type = auth_type.lower() + except (ValueError, AttributeError): + return ds.WWWAuthenticate(value.strip().lower(), on_update=on_update) + return ds.WWWAuthenticate(auth_type, parse_dict_header(auth_info), on_update) + + +def parse_if_range_header(value: t.Optional[str]) -> "ds.IfRange": + """Parses an if-range header which can be an etag or a date. Returns + a :class:`~werkzeug.datastructures.IfRange` object. + + .. versionchanged:: 2.0 + If the value represents a datetime, it is timezone-aware. + + .. versionadded:: 0.7 + """ + if not value: + return ds.IfRange() + date = parse_date(value) + if date is not None: + return ds.IfRange(date=date) + # drop weakness information + return ds.IfRange(unquote_etag(value)[0]) + + +def parse_range_header( + value: t.Optional[str], make_inclusive: bool = True +) -> t.Optional["ds.Range"]: + """Parses a range header into a :class:`~werkzeug.datastructures.Range` + object. If the header is missing or malformed `None` is returned. + `ranges` is a list of ``(start, stop)`` tuples where the ranges are + non-inclusive. + + .. versionadded:: 0.7 + """ + if not value or "=" not in value: + return None + + ranges = [] + last_end = 0 + units, rng = value.split("=", 1) + units = units.strip().lower() + + for item in rng.split(","): + item = item.strip() + if "-" not in item: + return None + if item.startswith("-"): + if last_end < 0: + return None + try: + begin = int(item) + except ValueError: + return None + end = None + last_end = -1 + elif "-" in item: + begin_str, end_str = item.split("-", 1) + begin_str = begin_str.strip() + end_str = end_str.strip() + if not begin_str.isdigit(): + return None + begin = int(begin_str) + if begin < last_end or last_end < 0: + return None + if end_str: + if not end_str.isdigit(): + return None + end = int(end_str) + 1 + if begin >= end: + return None + else: + end = None + last_end = end if end is not None else -1 + ranges.append((begin, end)) + + return ds.Range(units, ranges) + + +def parse_content_range_header( + value: t.Optional[str], + on_update: t.Optional[t.Callable[["ds.ContentRange"], None]] = None, +) -> t.Optional["ds.ContentRange"]: + """Parses a range header into a + :class:`~werkzeug.datastructures.ContentRange` object or `None` if + parsing is not possible. + + .. versionadded:: 0.7 + + :param value: a content range header to be parsed. + :param on_update: an optional callable that is called every time a value + on the :class:`~werkzeug.datastructures.ContentRange` + object is changed. + """ + if value is None: + return None + try: + units, rangedef = (value or "").strip().split(None, 1) + except ValueError: + return None + + if "/" not in rangedef: + return None + rng, length_str = rangedef.split("/", 1) + if length_str == "*": + length = None + elif length_str.isdigit(): + length = int(length_str) + else: + return None + + if rng == "*": + return ds.ContentRange(units, None, None, length, on_update=on_update) + elif "-" not in rng: + return None + + start_str, stop_str = rng.split("-", 1) + try: + start = int(start_str) + stop = int(stop_str) + 1 + except ValueError: + return None + + if is_byte_range_valid(start, stop, length): + return ds.ContentRange(units, start, stop, length, on_update=on_update) + + return None + + +def quote_etag(etag: str, weak: bool = False) -> str: + """Quote an etag. + + :param etag: the etag to quote. + :param weak: set to `True` to tag it "weak". + """ + if '"' in etag: + raise ValueError("invalid etag") + etag = f'"{etag}"' + if weak: + etag = f"W/{etag}" + return etag + + +def unquote_etag( + etag: t.Optional[str], +) -> t.Union[t.Tuple[str, bool], t.Tuple[None, None]]: + """Unquote a single etag: + + >>> unquote_etag('W/"bar"') + ('bar', True) + >>> unquote_etag('"bar"') + ('bar', False) + + :param etag: the etag identifier to unquote. + :return: a ``(etag, weak)`` tuple. + """ + if not etag: + return None, None + etag = etag.strip() + weak = False + if etag.startswith(("W/", "w/")): + weak = True + etag = etag[2:] + if etag[:1] == etag[-1:] == '"': + etag = etag[1:-1] + return etag, weak + + +def parse_etags(value: t.Optional[str]) -> "ds.ETags": + """Parse an etag header. + + :param value: the tag header to parse + :return: an :class:`~werkzeug.datastructures.ETags` object. + """ + if not value: + return ds.ETags() + strong = [] + weak = [] + end = len(value) + pos = 0 + while pos < end: + match = _etag_re.match(value, pos) + if match is None: + break + is_weak, quoted, raw = match.groups() + if raw == "*": + return ds.ETags(star_tag=True) + elif quoted: + raw = quoted + if is_weak: + weak.append(raw) + else: + strong.append(raw) + pos = match.end() + return ds.ETags(strong, weak) + + +def generate_etag(data: bytes) -> str: + """Generate an etag for some data. + + .. versionchanged:: 2.0 + Use SHA-1. MD5 may not be available in some environments. + """ + return sha1(data).hexdigest() + + +def parse_date(value: t.Optional[str]) -> t.Optional[datetime]: + """Parse an :rfc:`2822` date into a timezone-aware + :class:`datetime.datetime` object, or ``None`` if parsing fails. + + This is a wrapper for :func:`email.utils.parsedate_to_datetime`. It + returns ``None`` if parsing fails instead of raising an exception, + and always returns a timezone-aware datetime object. If the string + doesn't have timezone information, it is assumed to be UTC. + + :param value: A string with a supported date format. + + .. versionchanged:: 2.0 + Return a timezone-aware datetime object. Use + ``email.utils.parsedate_to_datetime``. + """ + if value is None: + return None + + try: + dt = email.utils.parsedate_to_datetime(value) + except (TypeError, ValueError): + return None + + if dt.tzinfo is None: + return dt.replace(tzinfo=timezone.utc) + + return dt + + +def cookie_date( + expires: t.Optional[t.Union[datetime, date, int, float, struct_time]] = None +) -> str: + """Format a datetime object or timestamp into an :rfc:`2822` date + string for ``Set-Cookie expires``. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Use :func:`http_date` instead. + """ + warnings.warn( + "'cookie_date' is deprecated and will be removed in Werkzeug" + " 2.1. Use 'http_date' instead.", + DeprecationWarning, + stacklevel=2, + ) + return http_date(expires) + + +def http_date( + timestamp: t.Optional[t.Union[datetime, date, int, float, struct_time]] = None +) -> str: + """Format a datetime object or timestamp into an :rfc:`2822` date + string. + + This is a wrapper for :func:`email.utils.format_datetime`. It + assumes naive datetime objects are in UTC instead of raising an + exception. + + :param timestamp: The datetime or timestamp to format. Defaults to + the current time. + + .. versionchanged:: 2.0 + Use ``email.utils.format_datetime``. Accept ``date`` objects. + """ + if isinstance(timestamp, date): + if not isinstance(timestamp, datetime): + # Assume plain date is midnight UTC. + timestamp = datetime.combine(timestamp, time(), tzinfo=timezone.utc) + else: + # Ensure datetime is timezone-aware. + timestamp = _dt_as_utc(timestamp) + + return email.utils.format_datetime(timestamp, usegmt=True) + + if isinstance(timestamp, struct_time): + timestamp = mktime(timestamp) + + return email.utils.formatdate(timestamp, usegmt=True) + + +def parse_age(value: t.Optional[str] = None) -> t.Optional[timedelta]: + """Parses a base-10 integer count of seconds into a timedelta. + + If parsing fails, the return value is `None`. + + :param value: a string consisting of an integer represented in base-10 + :return: a :class:`datetime.timedelta` object or `None`. + """ + if not value: + return None + try: + seconds = int(value) + except ValueError: + return None + if seconds < 0: + return None + try: + return timedelta(seconds=seconds) + except OverflowError: + return None + + +def dump_age(age: t.Optional[t.Union[timedelta, int]] = None) -> t.Optional[str]: + """Formats the duration as a base-10 integer. + + :param age: should be an integer number of seconds, + a :class:`datetime.timedelta` object, or, + if the age is unknown, `None` (default). + """ + if age is None: + return None + if isinstance(age, timedelta): + age = int(age.total_seconds()) + else: + age = int(age) + + if age < 0: + raise ValueError("age cannot be negative") + + return str(age) + + +def is_resource_modified( + environ: "WSGIEnvironment", + etag: t.Optional[str] = None, + data: t.Optional[bytes] = None, + last_modified: t.Optional[t.Union[datetime, str]] = None, + ignore_if_range: bool = True, +) -> bool: + """Convenience method for conditional requests. + + :param environ: the WSGI environment of the request to be checked. + :param etag: the etag for the response for comparison. + :param data: or alternatively the data of the response to automatically + generate an etag using :func:`generate_etag`. + :param last_modified: an optional date of the last modification. + :param ignore_if_range: If `False`, `If-Range` header will be taken into + account. + :return: `True` if the resource was modified, otherwise `False`. + + .. versionchanged:: 2.0 + SHA-1 is used to generate an etag value for the data. MD5 may + not be available in some environments. + + .. versionchanged:: 1.0.0 + The check is run for methods other than ``GET`` and ``HEAD``. + """ + if etag is None and data is not None: + etag = generate_etag(data) + elif data is not None: + raise TypeError("both data and etag given") + + unmodified = False + if isinstance(last_modified, str): + last_modified = parse_date(last_modified) + + # HTTP doesn't use microsecond, remove it to avoid false positive + # comparisons. Mark naive datetimes as UTC. + if last_modified is not None: + last_modified = _dt_as_utc(last_modified.replace(microsecond=0)) + + if_range = None + if not ignore_if_range and "HTTP_RANGE" in environ: + # https://tools.ietf.org/html/rfc7233#section-3.2 + # A server MUST ignore an If-Range header field received in a request + # that does not contain a Range header field. + if_range = parse_if_range_header(environ.get("HTTP_IF_RANGE")) + + if if_range is not None and if_range.date is not None: + modified_since: t.Optional[datetime] = if_range.date + else: + modified_since = parse_date(environ.get("HTTP_IF_MODIFIED_SINCE")) + + if modified_since and last_modified and last_modified <= modified_since: + unmodified = True + + if etag: + etag, _ = unquote_etag(etag) + etag = t.cast(str, etag) + + if if_range is not None and if_range.etag is not None: + unmodified = parse_etags(if_range.etag).contains(etag) + else: + if_none_match = parse_etags(environ.get("HTTP_IF_NONE_MATCH")) + if if_none_match: + # https://tools.ietf.org/html/rfc7232#section-3.2 + # "A recipient MUST use the weak comparison function when comparing + # entity-tags for If-None-Match" + unmodified = if_none_match.contains_weak(etag) + + # https://tools.ietf.org/html/rfc7232#section-3.1 + # "Origin server MUST use the strong comparison function when + # comparing entity-tags for If-Match" + if_match = parse_etags(environ.get("HTTP_IF_MATCH")) + if if_match: + unmodified = not if_match.is_strong(etag) + + return not unmodified + + +def remove_entity_headers( + headers: t.Union["ds.Headers", t.List[t.Tuple[str, str]]], + allowed: t.Iterable[str] = ("expires", "content-location"), +) -> None: + """Remove all entity headers from a list or :class:`Headers` object. This + operation works in-place. `Expires` and `Content-Location` headers are + by default not removed. The reason for this is :rfc:`2616` section + 10.3.5 which specifies some entity headers that should be sent. + + .. versionchanged:: 0.5 + added `allowed` parameter. + + :param headers: a list or :class:`Headers` object. + :param allowed: a list of headers that should still be allowed even though + they are entity headers. + """ + allowed = {x.lower() for x in allowed} + headers[:] = [ + (key, value) + for key, value in headers + if not is_entity_header(key) or key.lower() in allowed + ] + + +def remove_hop_by_hop_headers( + headers: t.Union["ds.Headers", t.List[t.Tuple[str, str]]] +) -> None: + """Remove all HTTP/1.1 "Hop-by-Hop" headers from a list or + :class:`Headers` object. This operation works in-place. + + .. versionadded:: 0.5 + + :param headers: a list or :class:`Headers` object. + """ + headers[:] = [ + (key, value) for key, value in headers if not is_hop_by_hop_header(key) + ] + + +def is_entity_header(header: str) -> bool: + """Check if a header is an entity header. + + .. versionadded:: 0.5 + + :param header: the header to test. + :return: `True` if it's an entity header, `False` otherwise. + """ + return header.lower() in _entity_headers + + +def is_hop_by_hop_header(header: str) -> bool: + """Check if a header is an HTTP/1.1 "Hop-by-Hop" header. + + .. versionadded:: 0.5 + + :param header: the header to test. + :return: `True` if it's an HTTP/1.1 "Hop-by-Hop" header, `False` otherwise. + """ + return header.lower() in _hop_by_hop_headers + + +def parse_cookie( + header: t.Union["WSGIEnvironment", str, bytes, None], + charset: str = "utf-8", + errors: str = "replace", + cls: t.Optional[t.Type["ds.MultiDict"]] = None, +) -> "ds.MultiDict[str, str]": + """Parse a cookie from a string or WSGI environ. + + The same key can be provided multiple times, the values are stored + in-order. The default :class:`MultiDict` will have the first value + first, and all values can be retrieved with + :meth:`MultiDict.getlist`. + + :param header: The cookie header as a string, or a WSGI environ dict + with a ``HTTP_COOKIE`` key. + :param charset: The charset for the cookie values. + :param errors: The error behavior for the charset decoding. + :param cls: A dict-like class to store the parsed cookies in. + Defaults to :class:`MultiDict`. + + .. versionchanged:: 1.0.0 + Returns a :class:`MultiDict` instead of a + ``TypeConversionDict``. + + .. versionchanged:: 0.5 + Returns a :class:`TypeConversionDict` instead of a regular dict. + The ``cls`` parameter was added. + """ + if isinstance(header, dict): + header = header.get("HTTP_COOKIE", "") + elif header is None: + header = "" + + # PEP 3333 sends headers through the environ as latin1 decoded + # strings. Encode strings back to bytes for parsing. + if isinstance(header, str): + header = header.encode("latin1", "replace") + + if cls is None: + cls = ds.MultiDict + + def _parse_pairs() -> t.Iterator[t.Tuple[str, str]]: + for key, val in _cookie_parse_impl(header): # type: ignore + key_str = _to_str(key, charset, errors, allow_none_charset=True) + + if not key_str: + continue + + val_str = _to_str(val, charset, errors, allow_none_charset=True) + yield key_str, val_str + + return cls(_parse_pairs()) + + +def dump_cookie( + key: str, + value: t.Union[bytes, str] = "", + max_age: t.Optional[t.Union[timedelta, int]] = None, + expires: t.Optional[t.Union[str, datetime, int, float]] = None, + path: t.Optional[str] = "/", + domain: t.Optional[str] = None, + secure: bool = False, + httponly: bool = False, + charset: str = "utf-8", + sync_expires: bool = True, + max_size: int = 4093, + samesite: t.Optional[str] = None, +) -> str: + """Create a Set-Cookie header without the ``Set-Cookie`` prefix. + + The return value is usually restricted to ascii as the vast majority + of values are properly escaped, but that is no guarantee. It's + tunneled through latin1 as required by :pep:`3333`. + + The return value is not ASCII safe if the key contains unicode + characters. This is technically against the specification but + happens in the wild. It's strongly recommended to not use + non-ASCII values for the keys. + + :param max_age: should be a number of seconds, or `None` (default) if + the cookie should last only as long as the client's + browser session. Additionally `timedelta` objects + are accepted, too. + :param expires: should be a `datetime` object or unix timestamp. + :param path: limits the cookie to a given path, per default it will + span the whole domain. + :param domain: Use this if you want to set a cross-domain cookie. For + example, ``domain=".example.com"`` will set a cookie + that is readable by the domain ``www.example.com``, + ``foo.example.com`` etc. Otherwise, a cookie will only + be readable by the domain that set it. + :param secure: The cookie will only be available via HTTPS + :param httponly: disallow JavaScript to access the cookie. This is an + extension to the cookie standard and probably not + supported by all browsers. + :param charset: the encoding for string values. + :param sync_expires: automatically set expires if max_age is defined + but expires not. + :param max_size: Warn if the final header value exceeds this size. The + default, 4093, should be safely `supported by most browsers + `_. Set to 0 to disable this check. + :param samesite: Limits the scope of the cookie such that it will + only be attached to requests if those requests are same-site. + + .. _`cookie`: http://browsercookielimits.squawky.net/ + + .. versionchanged:: 1.0.0 + The string ``'None'`` is accepted for ``samesite``. + """ + key = _to_bytes(key, charset) + value = _to_bytes(value, charset) + + if path is not None: + from .urls import iri_to_uri + + path = iri_to_uri(path, charset) + + domain = _make_cookie_domain(domain) + + if isinstance(max_age, timedelta): + max_age = int(max_age.total_seconds()) + + if expires is not None: + if not isinstance(expires, str): + expires = http_date(expires) + elif max_age is not None and sync_expires: + expires = http_date(datetime.now(tz=timezone.utc).timestamp() + max_age) + + if samesite is not None: + samesite = samesite.title() + + if samesite not in {"Strict", "Lax", "None"}: + raise ValueError("SameSite must be 'Strict', 'Lax', or 'None'.") + + buf = [key + b"=" + _cookie_quote(value)] + + # XXX: In theory all of these parameters that are not marked with `None` + # should be quoted. Because stdlib did not quote it before I did not + # want to introduce quoting there now. + for k, v, q in ( + (b"Domain", domain, True), + (b"Expires", expires, False), + (b"Max-Age", max_age, False), + (b"Secure", secure, None), + (b"HttpOnly", httponly, None), + (b"Path", path, False), + (b"SameSite", samesite, False), + ): + if q is None: + if v: + buf.append(k) + continue + + if v is None: + continue + + tmp = bytearray(k) + if not isinstance(v, (bytes, bytearray)): + v = _to_bytes(str(v), charset) + if q: + v = _cookie_quote(v) + tmp += b"=" + v + buf.append(bytes(tmp)) + + # The return value will be an incorrectly encoded latin1 header for + # consistency with the headers object. + rv = b"; ".join(buf) + rv = rv.decode("latin1") + + # Warn if the final value of the cookie is larger than the limit. If the + # cookie is too large, then it may be silently ignored by the browser, + # which can be quite hard to debug. + cookie_size = len(rv) + + if max_size and cookie_size > max_size: + value_size = len(value) + warnings.warn( + f"The {key.decode(charset)!r} cookie is too large: the value was" + f" {value_size} bytes but the" + f" header required {cookie_size - value_size} extra bytes. The final size" + f" was {cookie_size} bytes but the limit is {max_size} bytes. Browsers may" + f" silently ignore cookies larger than this.", + stacklevel=2, + ) + + return rv + + +def is_byte_range_valid( + start: t.Optional[int], stop: t.Optional[int], length: t.Optional[int] +) -> bool: + """Checks if a given byte content range is valid for the given length. + + .. versionadded:: 0.7 + """ + if (start is None) != (stop is None): + return False + elif start is None: + return length is None or length >= 0 + elif length is None: + return 0 <= start < stop # type: ignore + elif start >= stop: # type: ignore + return False + return 0 <= start < length + + +# circular dependencies +from . import datastructures as ds diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/local.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/local.py new file mode 100644 index 000000000..b4dee7b4d --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/local.py @@ -0,0 +1,677 @@ +import copy +import math +import operator +import sys +import typing as t +import warnings +from functools import partial +from functools import update_wrapper + +from .wsgi import ClosingIterator + +if t.TYPE_CHECKING: + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + +try: + from greenlet import getcurrent as _get_ident +except ImportError: + from threading import get_ident as _get_ident + + +def get_ident() -> int: + warnings.warn( + "'get_ident' is deprecated and will be removed in Werkzeug" + " 2.1. Use 'greenlet.getcurrent' or 'threading.get_ident' for" + " previous behavior.", + DeprecationWarning, + stacklevel=2, + ) + return _get_ident() # type: ignore + + +class _CannotUseContextVar(Exception): + pass + + +try: + from contextvars import ContextVar + + if "gevent" in sys.modules or "eventlet" in sys.modules: + # Both use greenlet, so first check it has patched + # ContextVars, Greenlet <0.4.17 does not. + import greenlet + + greenlet_patched = getattr(greenlet, "GREENLET_USE_CONTEXT_VARS", False) + + if not greenlet_patched: + # If Gevent is used, check it has patched ContextVars, + # <20.5 does not. + try: + from gevent.monkey import is_object_patched + except ImportError: + # Gevent isn't used, but Greenlet is and hasn't patched + raise _CannotUseContextVar() from None + else: + if is_object_patched("threading", "local") and not is_object_patched( + "contextvars", "ContextVar" + ): + raise _CannotUseContextVar() + + def __release_local__(storage: t.Any) -> None: + # Can remove when support for non-stdlib ContextVars is + # removed, see "Fake" version below. + storage.set({}) + + +except (ImportError, _CannotUseContextVar): + + class ContextVar: # type: ignore + """A fake ContextVar based on the previous greenlet/threading + ident function. Used on Python 3.6, eventlet, and old versions + of gevent. + """ + + def __init__(self, _name: str) -> None: + self.storage: t.Dict[int, t.Dict[str, t.Any]] = {} + + def get(self, default: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + return self.storage.get(_get_ident(), default) + + def set(self, value: t.Dict[str, t.Any]) -> None: + self.storage[_get_ident()] = value + + def __release_local__(storage: t.Any) -> None: + # Special version to ensure that the storage is cleaned up on + # release. + storage.storage.pop(_get_ident(), None) + + +def release_local(local: t.Union["Local", "LocalStack"]) -> None: + """Releases the contents of the local for the current context. + This makes it possible to use locals without a manager. + + Example:: + + >>> loc = Local() + >>> loc.foo = 42 + >>> release_local(loc) + >>> hasattr(loc, 'foo') + False + + With this function one can release :class:`Local` objects as well + as :class:`LocalStack` objects. However it is not possible to + release data held by proxies that way, one always has to retain + a reference to the underlying local object in order to be able + to release it. + + .. versionadded:: 0.6.1 + """ + local.__release_local__() + + +class Local: + __slots__ = ("_storage",) + + def __init__(self) -> None: + object.__setattr__(self, "_storage", ContextVar("local_storage")) + + @property + def __storage__(self) -> t.Dict[str, t.Any]: + warnings.warn( + "'__storage__' is deprecated and will be removed in Werkzeug 2.1.", + DeprecationWarning, + stacklevel=2, + ) + return self._storage.get({}) # type: ignore + + @property + def __ident_func__(self) -> t.Callable[[], int]: + warnings.warn( + "'__ident_func__' is deprecated and will be removed in" + " Werkzeug 2.1. It should not be used in Python 3.7+.", + DeprecationWarning, + stacklevel=2, + ) + return _get_ident # type: ignore + + @__ident_func__.setter + def __ident_func__(self, func: t.Callable[[], int]) -> None: + warnings.warn( + "'__ident_func__' is deprecated and will be removed in" + " Werkzeug 2.1. Setting it no longer has any effect.", + DeprecationWarning, + stacklevel=2, + ) + + def __iter__(self) -> t.Iterator[t.Tuple[int, t.Any]]: + return iter(self._storage.get({}).items()) + + def __call__(self, proxy: str) -> "LocalProxy": + """Create a proxy for a name.""" + return LocalProxy(self, proxy) + + def __release_local__(self) -> None: + __release_local__(self._storage) + + def __getattr__(self, name: str) -> t.Any: + values = self._storage.get({}) + try: + return values[name] + except KeyError: + raise AttributeError(name) from None + + def __setattr__(self, name: str, value: t.Any) -> None: + values = self._storage.get({}).copy() + values[name] = value + self._storage.set(values) + + def __delattr__(self, name: str) -> None: + values = self._storage.get({}).copy() + try: + del values[name] + self._storage.set(values) + except KeyError: + raise AttributeError(name) from None + + +class LocalStack: + """This class works similar to a :class:`Local` but keeps a stack + of objects instead. This is best explained with an example:: + + >>> ls = LocalStack() + >>> ls.push(42) + >>> ls.top + 42 + >>> ls.push(23) + >>> ls.top + 23 + >>> ls.pop() + 23 + >>> ls.top + 42 + + They can be force released by using a :class:`LocalManager` or with + the :func:`release_local` function but the correct way is to pop the + item from the stack after using. When the stack is empty it will + no longer be bound to the current context (and as such released). + + By calling the stack without arguments it returns a proxy that resolves to + the topmost item on the stack. + + .. versionadded:: 0.6.1 + """ + + def __init__(self) -> None: + self._local = Local() + + def __release_local__(self) -> None: + self._local.__release_local__() + + @property + def __ident_func__(self) -> t.Callable[[], int]: + return self._local.__ident_func__ + + @__ident_func__.setter + def __ident_func__(self, value: t.Callable[[], int]) -> None: + object.__setattr__(self._local, "__ident_func__", value) + + def __call__(self) -> "LocalProxy": + def _lookup() -> t.Any: + rv = self.top + if rv is None: + raise RuntimeError("object unbound") + return rv + + return LocalProxy(_lookup) + + def push(self, obj: t.Any) -> t.List[t.Any]: + """Pushes a new item to the stack""" + rv = getattr(self._local, "stack", []).copy() + rv.append(obj) + self._local.stack = rv + return rv # type: ignore + + def pop(self) -> t.Any: + """Removes the topmost item from the stack, will return the + old value or `None` if the stack was already empty. + """ + stack = getattr(self._local, "stack", None) + if stack is None: + return None + elif len(stack) == 1: + release_local(self._local) + return stack[-1] + else: + return stack.pop() + + @property + def top(self) -> t.Any: + """The topmost item on the stack. If the stack is empty, + `None` is returned. + """ + try: + return self._local.stack[-1] + except (AttributeError, IndexError): + return None + + +class LocalManager: + """Local objects cannot manage themselves. For that you need a local + manager. You can pass a local manager multiple locals or add them + later by appending them to `manager.locals`. Every time the manager + cleans up, it will clean up all the data left in the locals for this + context. + + .. versionchanged:: 2.0 + ``ident_func`` is deprecated and will be removed in Werkzeug + 2.1. + + .. versionchanged:: 0.6.1 + The :func:`release_local` function can be used instead of a + manager. + + .. versionchanged:: 0.7 + The ``ident_func`` parameter was added. + """ + + def __init__( + self, + locals: t.Optional[t.Iterable[t.Union[Local, LocalStack]]] = None, + ident_func: None = None, + ) -> None: + if locals is None: + self.locals = [] + elif isinstance(locals, Local): + self.locals = [locals] + else: + self.locals = list(locals) + + if ident_func is not None: + warnings.warn( + "'ident_func' is deprecated and will be removed in" + " Werkzeug 2.1. Setting it no longer has any effect.", + DeprecationWarning, + stacklevel=2, + ) + + @property + def ident_func(self) -> t.Callable[[], int]: + warnings.warn( + "'ident_func' is deprecated and will be removed in Werkzeug 2.1.", + DeprecationWarning, + stacklevel=2, + ) + return _get_ident # type: ignore + + @ident_func.setter + def ident_func(self, func: t.Callable[[], int]) -> None: + warnings.warn( + "'ident_func' is deprecated and will be removedin Werkzeug" + " 2.1. Setting it no longer has any effect.", + DeprecationWarning, + stacklevel=2, + ) + + def get_ident(self) -> int: + """Return the context identifier the local objects use internally for + this context. You cannot override this method to change the behavior + but use it to link other context local objects (such as SQLAlchemy's + scoped sessions) to the Werkzeug locals. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. + + .. versionchanged:: 0.7 + You can pass a different ident function to the local manager that + will then be propagated to all the locals passed to the + constructor. + """ + warnings.warn( + "'get_ident' is deprecated and will be removed in Werkzeug 2.1.", + DeprecationWarning, + stacklevel=2, + ) + return self.ident_func() + + def cleanup(self) -> None: + """Manually clean up the data in the locals for this context. Call + this at the end of the request or use `make_middleware()`. + """ + for local in self.locals: + release_local(local) + + def make_middleware(self, app: "WSGIApplication") -> "WSGIApplication": + """Wrap a WSGI application so that cleaning up happens after + request end. + """ + + def application( + environ: "WSGIEnvironment", start_response: "StartResponse" + ) -> t.Iterable[bytes]: + return ClosingIterator(app(environ, start_response), self.cleanup) + + return application + + def middleware(self, func: "WSGIApplication") -> "WSGIApplication": + """Like `make_middleware` but for decorating functions. + + Example usage:: + + @manager.middleware + def application(environ, start_response): + ... + + The difference to `make_middleware` is that the function passed + will have all the arguments copied from the inner application + (name, docstring, module). + """ + return update_wrapper(self.make_middleware(func), func) + + def __repr__(self) -> str: + return f"<{type(self).__name__} storages: {len(self.locals)}>" + + +class _ProxyLookup: + """Descriptor that handles proxied attribute lookup for + :class:`LocalProxy`. + + :param f: The built-in function this attribute is accessed through. + Instead of looking up the special method, the function call + is redone on the object. + :param fallback: Call this method if the proxy is unbound instead of + raising a :exc:`RuntimeError`. + :param class_value: Value to return when accessed from the class. + Used for ``__doc__`` so building docs still works. + """ + + __slots__ = ("bind_f", "fallback", "class_value", "name") + + def __init__( + self, + f: t.Optional[t.Callable] = None, + fallback: t.Optional[t.Callable] = None, + class_value: t.Optional[t.Any] = None, + ) -> None: + bind_f: t.Optional[t.Callable[["LocalProxy", t.Any], t.Callable]] + + if hasattr(f, "__get__"): + # A Python function, can be turned into a bound method. + + def bind_f(instance: "LocalProxy", obj: t.Any) -> t.Callable: + return f.__get__(obj, type(obj)) # type: ignore + + elif f is not None: + # A C function, use partial to bind the first argument. + + def bind_f(instance: "LocalProxy", obj: t.Any) -> t.Callable: + return partial(f, obj) # type: ignore + + else: + # Use getattr, which will produce a bound method. + bind_f = None + + self.bind_f = bind_f + self.fallback = fallback + self.class_value = class_value + + def __set_name__(self, owner: "LocalProxy", name: str) -> None: + self.name = name + + def __get__(self, instance: "LocalProxy", owner: t.Optional[type] = None) -> t.Any: + if instance is None: + if self.class_value is not None: + return self.class_value + + return self + + try: + obj = instance._get_current_object() + except RuntimeError: + if self.fallback is None: + raise + + return self.fallback.__get__(instance, owner) # type: ignore + + if self.bind_f is not None: + return self.bind_f(instance, obj) + + return getattr(obj, self.name) + + def __repr__(self) -> str: + return f"proxy {self.name}" + + def __call__(self, instance: "LocalProxy", *args: t.Any, **kwargs: t.Any) -> t.Any: + """Support calling unbound methods from the class. For example, + this happens with ``copy.copy``, which does + ``type(x).__copy__(x)``. ``type(x)`` can't be proxied, so it + returns the proxy type and descriptor. + """ + return self.__get__(instance, type(instance))(*args, **kwargs) + + +class _ProxyIOp(_ProxyLookup): + """Look up an augmented assignment method on a proxied object. The + method is wrapped to return the proxy instead of the object. + """ + + __slots__ = () + + def __init__( + self, f: t.Optional[t.Callable] = None, fallback: t.Optional[t.Callable] = None + ) -> None: + super().__init__(f, fallback) + + def bind_f(instance: "LocalProxy", obj: t.Any) -> t.Callable: + def i_op(self: t.Any, other: t.Any) -> "LocalProxy": + f(self, other) # type: ignore + return instance + + return i_op.__get__(obj, type(obj)) # type: ignore + + self.bind_f = bind_f + + +def _l_to_r_op(op: F) -> F: + """Swap the argument order to turn an l-op into an r-op.""" + + def r_op(obj: t.Any, other: t.Any) -> t.Any: + return op(other, obj) + + return t.cast(F, r_op) + + +class LocalProxy: + """A proxy to the object bound to a :class:`Local`. All operations + on the proxy are forwarded to the bound object. If no object is + bound, a :exc:`RuntimeError` is raised. + + .. code-block:: python + + from werkzeug.local import Local + l = Local() + + # a proxy to whatever l.user is set to + user = l("user") + + from werkzeug.local import LocalStack + _request_stack = LocalStack() + + # a proxy to _request_stack.top + request = _request_stack() + + # a proxy to the session attribute of the request proxy + session = LocalProxy(lambda: request.session) + + ``__repr__`` and ``__class__`` are forwarded, so ``repr(x)`` and + ``isinstance(x, cls)`` will look like the proxied object. Use + ``issubclass(type(x), LocalProxy)`` to check if an object is a + proxy. + + .. code-block:: python + + repr(user) # + isinstance(user, User) # True + issubclass(type(user), LocalProxy) # True + + :param local: The :class:`Local` or callable that provides the + proxied object. + :param name: The attribute name to look up on a :class:`Local`. Not + used if a callable is given. + + .. versionchanged:: 2.0 + Updated proxied attributes and methods to reflect the current + data model. + + .. versionchanged:: 0.6.1 + The class can be instantiated with a callable. + """ + + __slots__ = ("__local", "__name", "__wrapped__") + + def __init__( + self, + local: t.Union["Local", t.Callable[[], t.Any]], + name: t.Optional[str] = None, + ) -> None: + object.__setattr__(self, "_LocalProxy__local", local) + object.__setattr__(self, "_LocalProxy__name", name) + + if callable(local) and not hasattr(local, "__release_local__"): + # "local" is a callable that is not an instance of Local or + # LocalManager: mark it as a wrapped function. + object.__setattr__(self, "__wrapped__", local) + + def _get_current_object(self) -> t.Any: + """Return the current object. This is useful if you want the real + object behind the proxy at a time for performance reasons or because + you want to pass the object into a different context. + """ + if not hasattr(self.__local, "__release_local__"): # type: ignore + return self.__local() # type: ignore + + try: + return getattr(self.__local, self.__name) # type: ignore + except AttributeError: + name = self.__name # type: ignore + raise RuntimeError(f"no object bound to {name}") from None + + __doc__ = _ProxyLookup( # type: ignore + class_value=__doc__, fallback=lambda self: type(self).__doc__ + ) + # __del__ should only delete the proxy + __repr__ = _ProxyLookup( # type: ignore + repr, fallback=lambda self: f"<{type(self).__name__} unbound>" + ) + __str__ = _ProxyLookup(str) # type: ignore + __bytes__ = _ProxyLookup(bytes) + __format__ = _ProxyLookup() # type: ignore + __lt__ = _ProxyLookup(operator.lt) + __le__ = _ProxyLookup(operator.le) + __eq__ = _ProxyLookup(operator.eq) # type: ignore + __ne__ = _ProxyLookup(operator.ne) # type: ignore + __gt__ = _ProxyLookup(operator.gt) + __ge__ = _ProxyLookup(operator.ge) + __hash__ = _ProxyLookup(hash) # type: ignore + __bool__ = _ProxyLookup(bool, fallback=lambda self: False) + __getattr__ = _ProxyLookup(getattr) + # __getattribute__ triggered through __getattr__ + __setattr__ = _ProxyLookup(setattr) # type: ignore + __delattr__ = _ProxyLookup(delattr) # type: ignore + __dir__ = _ProxyLookup(dir, fallback=lambda self: []) # type: ignore + # __get__ (proxying descriptor not supported) + # __set__ (descriptor) + # __delete__ (descriptor) + # __set_name__ (descriptor) + # __objclass__ (descriptor) + # __slots__ used by proxy itself + # __dict__ (__getattr__) + # __weakref__ (__getattr__) + # __init_subclass__ (proxying metaclass not supported) + # __prepare__ (metaclass) + __class__ = _ProxyLookup(fallback=lambda self: type(self)) # type: ignore + __instancecheck__ = _ProxyLookup(lambda self, other: isinstance(other, self)) + __subclasscheck__ = _ProxyLookup(lambda self, other: issubclass(other, self)) + # __class_getitem__ triggered through __getitem__ + __call__ = _ProxyLookup(lambda self, *args, **kwargs: self(*args, **kwargs)) + __len__ = _ProxyLookup(len) + __length_hint__ = _ProxyLookup(operator.length_hint) + __getitem__ = _ProxyLookup(operator.getitem) + __setitem__ = _ProxyLookup(operator.setitem) + __delitem__ = _ProxyLookup(operator.delitem) + # __missing__ triggered through __getitem__ + __iter__ = _ProxyLookup(iter) + __next__ = _ProxyLookup(next) + __reversed__ = _ProxyLookup(reversed) + __contains__ = _ProxyLookup(operator.contains) + __add__ = _ProxyLookup(operator.add) + __sub__ = _ProxyLookup(operator.sub) + __mul__ = _ProxyLookup(operator.mul) + __matmul__ = _ProxyLookup(operator.matmul) + __truediv__ = _ProxyLookup(operator.truediv) + __floordiv__ = _ProxyLookup(operator.floordiv) + __mod__ = _ProxyLookup(operator.mod) + __divmod__ = _ProxyLookup(divmod) + __pow__ = _ProxyLookup(pow) + __lshift__ = _ProxyLookup(operator.lshift) + __rshift__ = _ProxyLookup(operator.rshift) + __and__ = _ProxyLookup(operator.and_) + __xor__ = _ProxyLookup(operator.xor) + __or__ = _ProxyLookup(operator.or_) + __radd__ = _ProxyLookup(_l_to_r_op(operator.add)) + __rsub__ = _ProxyLookup(_l_to_r_op(operator.sub)) + __rmul__ = _ProxyLookup(_l_to_r_op(operator.mul)) + __rmatmul__ = _ProxyLookup(_l_to_r_op(operator.matmul)) + __rtruediv__ = _ProxyLookup(_l_to_r_op(operator.truediv)) + __rfloordiv__ = _ProxyLookup(_l_to_r_op(operator.floordiv)) + __rmod__ = _ProxyLookup(_l_to_r_op(operator.mod)) + __rdivmod__ = _ProxyLookup(_l_to_r_op(divmod)) + __rpow__ = _ProxyLookup(_l_to_r_op(pow)) + __rlshift__ = _ProxyLookup(_l_to_r_op(operator.lshift)) + __rrshift__ = _ProxyLookup(_l_to_r_op(operator.rshift)) + __rand__ = _ProxyLookup(_l_to_r_op(operator.and_)) + __rxor__ = _ProxyLookup(_l_to_r_op(operator.xor)) + __ror__ = _ProxyLookup(_l_to_r_op(operator.or_)) + __iadd__ = _ProxyIOp(operator.iadd) + __isub__ = _ProxyIOp(operator.isub) + __imul__ = _ProxyIOp(operator.imul) + __imatmul__ = _ProxyIOp(operator.imatmul) + __itruediv__ = _ProxyIOp(operator.itruediv) + __ifloordiv__ = _ProxyIOp(operator.ifloordiv) + __imod__ = _ProxyIOp(operator.imod) + __ipow__ = _ProxyIOp(operator.ipow) + __ilshift__ = _ProxyIOp(operator.ilshift) + __irshift__ = _ProxyIOp(operator.irshift) + __iand__ = _ProxyIOp(operator.iand) + __ixor__ = _ProxyIOp(operator.ixor) + __ior__ = _ProxyIOp(operator.ior) + __neg__ = _ProxyLookup(operator.neg) + __pos__ = _ProxyLookup(operator.pos) + __abs__ = _ProxyLookup(abs) + __invert__ = _ProxyLookup(operator.invert) + __complex__ = _ProxyLookup(complex) + __int__ = _ProxyLookup(int) + __float__ = _ProxyLookup(float) + __index__ = _ProxyLookup(operator.index) + __round__ = _ProxyLookup(round) + __trunc__ = _ProxyLookup(math.trunc) + __floor__ = _ProxyLookup(math.floor) + __ceil__ = _ProxyLookup(math.ceil) + __enter__ = _ProxyLookup() + __exit__ = _ProxyLookup() + __await__ = _ProxyLookup() + __aiter__ = _ProxyLookup() + __anext__ = _ProxyLookup() + __aenter__ = _ProxyLookup() + __aexit__ = _ProxyLookup() + __copy__ = _ProxyLookup(copy.copy) + __deepcopy__ = _ProxyLookup(copy.deepcopy) + # __getnewargs_ex__ (pickle through proxy not supported) + # __getnewargs__ (pickle) + # __getstate__ (pickle) + # __setstate__ (pickle) + # __reduce__ (pickle) + # __reduce_ex__ (pickle) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__init__.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__init__.py new file mode 100644 index 000000000..6ddcf7f5c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__init__.py @@ -0,0 +1,22 @@ +""" +Middleware +========== + +A WSGI middleware is a WSGI application that wraps another application +in order to observe or change its behavior. Werkzeug provides some +middleware for common use cases. + +.. toctree:: + :maxdepth: 1 + + proxy_fix + shared_data + dispatcher + http_proxy + lint + profiler + +The :doc:`interactive debugger ` is also a middleware that can +be applied manually, although it is typically used automatically with +the :doc:`development server `. +""" diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..1a49b33e6 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/dispatcher.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/dispatcher.cpython-38.pyc new file mode 100644 index 000000000..387f5ed90 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/dispatcher.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/http_proxy.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/http_proxy.cpython-38.pyc new file mode 100644 index 000000000..0496eb77c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/http_proxy.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/lint.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/lint.cpython-38.pyc new file mode 100644 index 000000000..10ad4c99d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/lint.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/profiler.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/profiler.cpython-38.pyc new file mode 100644 index 000000000..5cfb8b0d9 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/profiler.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/proxy_fix.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/proxy_fix.cpython-38.pyc new file mode 100644 index 000000000..50a237e73 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/proxy_fix.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/shared_data.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/shared_data.cpython-38.pyc new file mode 100644 index 000000000..b957db7d5 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/__pycache__/shared_data.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/dispatcher.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/dispatcher.py new file mode 100644 index 000000000..ace1c7504 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/dispatcher.py @@ -0,0 +1,78 @@ +""" +Application Dispatcher +====================== + +This middleware creates a single WSGI application that dispatches to +multiple other WSGI applications mounted at different URL paths. + +A common example is writing a Single Page Application, where you have a +backend API and a frontend written in JavaScript that does the routing +in the browser rather than requesting different pages from the server. +The frontend is a single HTML and JS file that should be served for any +path besides "/api". + +This example dispatches to an API app under "/api", an admin app +under "/admin", and an app that serves frontend files for all other +requests:: + + app = DispatcherMiddleware(serve_frontend, { + '/api': api_app, + '/admin': admin_app, + }) + +In production, you might instead handle this at the HTTP server level, +serving files or proxying to application servers based on location. The +API and admin apps would each be deployed with a separate WSGI server, +and the static files would be served directly by the HTTP server. + +.. autoclass:: DispatcherMiddleware + +:copyright: 2007 Pallets +:license: BSD-3-Clause +""" +import typing as t + +if t.TYPE_CHECKING: + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + + +class DispatcherMiddleware: + """Combine multiple applications as a single WSGI application. + Requests are dispatched to an application based on the path it is + mounted under. + + :param app: The WSGI application to dispatch to if the request + doesn't match a mounted path. + :param mounts: Maps path prefixes to applications for dispatching. + """ + + def __init__( + self, + app: "WSGIApplication", + mounts: t.Optional[t.Dict[str, "WSGIApplication"]] = None, + ) -> None: + self.app = app + self.mounts = mounts or {} + + def __call__( + self, environ: "WSGIEnvironment", start_response: "StartResponse" + ) -> t.Iterable[bytes]: + script = environ.get("PATH_INFO", "") + path_info = "" + + while "/" in script: + if script in self.mounts: + app = self.mounts[script] + break + + script, last_item = script.rsplit("/", 1) + path_info = f"/{last_item}{path_info}" + else: + app = self.mounts.get(script, self.app) + + original_script_name = environ.get("SCRIPT_NAME", "") + environ["SCRIPT_NAME"] = original_script_name + script + environ["PATH_INFO"] = path_info + return app(environ, start_response) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/http_proxy.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/http_proxy.py new file mode 100644 index 000000000..1cde458df --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/http_proxy.py @@ -0,0 +1,230 @@ +""" +Basic HTTP Proxy +================ + +.. autoclass:: ProxyMiddleware + +:copyright: 2007 Pallets +:license: BSD-3-Clause +""" +import typing as t +from http import client + +from ..datastructures import EnvironHeaders +from ..http import is_hop_by_hop_header +from ..urls import url_parse +from ..urls import url_quote +from ..wsgi import get_input_stream + +if t.TYPE_CHECKING: + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + + +class ProxyMiddleware: + """Proxy requests under a path to an external server, routing other + requests to the app. + + This middleware can only proxy HTTP requests, as HTTP is the only + protocol handled by the WSGI server. Other protocols, such as + WebSocket requests, cannot be proxied at this layer. This should + only be used for development, in production a real proxy server + should be used. + + The middleware takes a dict mapping a path prefix to a dict + describing the host to be proxied to:: + + app = ProxyMiddleware(app, { + "/static/": { + "target": "http://127.0.0.1:5001/", + } + }) + + Each host has the following options: + + ``target``: + The target URL to dispatch to. This is required. + ``remove_prefix``: + Whether to remove the prefix from the URL before dispatching it + to the target. The default is ``False``. + ``host``: + ``""`` (default): + The host header is automatically rewritten to the URL of the + target. + ``None``: + The host header is unmodified from the client request. + Any other value: + The host header is overwritten with the value. + ``headers``: + A dictionary of headers to be sent with the request to the + target. The default is ``{}``. + ``ssl_context``: + A :class:`ssl.SSLContext` defining how to verify requests if the + target is HTTPS. The default is ``None``. + + In the example above, everything under ``"/static/"`` is proxied to + the server on port 5001. The host header is rewritten to the target, + and the ``"/static/"`` prefix is removed from the URLs. + + :param app: The WSGI application to wrap. + :param targets: Proxy target configurations. See description above. + :param chunk_size: Size of chunks to read from input stream and + write to target. + :param timeout: Seconds before an operation to a target fails. + + .. versionadded:: 0.14 + """ + + def __init__( + self, + app: "WSGIApplication", + targets: t.Mapping[str, t.Dict[str, t.Any]], + chunk_size: int = 2 << 13, + timeout: int = 10, + ) -> None: + def _set_defaults(opts: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + opts.setdefault("remove_prefix", False) + opts.setdefault("host", "") + opts.setdefault("headers", {}) + opts.setdefault("ssl_context", None) + return opts + + self.app = app + self.targets = { + f"/{k.strip('/')}/": _set_defaults(v) for k, v in targets.items() + } + self.chunk_size = chunk_size + self.timeout = timeout + + def proxy_to( + self, opts: t.Dict[str, t.Any], path: str, prefix: str + ) -> "WSGIApplication": + target = url_parse(opts["target"]) + host = t.cast(str, target.ascii_host) + + def application( + environ: "WSGIEnvironment", start_response: "StartResponse" + ) -> t.Iterable[bytes]: + headers = list(EnvironHeaders(environ).items()) + headers[:] = [ + (k, v) + for k, v in headers + if not is_hop_by_hop_header(k) + and k.lower() not in ("content-length", "host") + ] + headers.append(("Connection", "close")) + + if opts["host"] == "": + headers.append(("Host", host)) + elif opts["host"] is None: + headers.append(("Host", environ["HTTP_HOST"])) + else: + headers.append(("Host", opts["host"])) + + headers.extend(opts["headers"].items()) + remote_path = path + + if opts["remove_prefix"]: + remote_path = remote_path[len(prefix) :].lstrip("/") + remote_path = f"{target.path.rstrip('/')}/{remote_path}" + + content_length = environ.get("CONTENT_LENGTH") + chunked = False + + if content_length not in ("", None): + headers.append(("Content-Length", content_length)) # type: ignore + elif content_length is not None: + headers.append(("Transfer-Encoding", "chunked")) + chunked = True + + try: + if target.scheme == "http": + con = client.HTTPConnection( + host, target.port or 80, timeout=self.timeout + ) + elif target.scheme == "https": + con = client.HTTPSConnection( + host, + target.port or 443, + timeout=self.timeout, + context=opts["ssl_context"], + ) + else: + raise RuntimeError( + "Target scheme must be 'http' or 'https', got" + f" {target.scheme!r}." + ) + + con.connect() + remote_url = url_quote(remote_path) + querystring = environ["QUERY_STRING"] + + if querystring: + remote_url = f"{remote_url}?{querystring}" + + con.putrequest(environ["REQUEST_METHOD"], remote_url, skip_host=True) + + for k, v in headers: + if k.lower() == "connection": + v = "close" + + con.putheader(k, v) + + con.endheaders() + stream = get_input_stream(environ) + + while True: + data = stream.read(self.chunk_size) + + if not data: + break + + if chunked: + con.send(b"%x\r\n%s\r\n" % (len(data), data)) + else: + con.send(data) + + resp = con.getresponse() + except OSError: + from ..exceptions import BadGateway + + return BadGateway()(environ, start_response) + + start_response( + f"{resp.status} {resp.reason}", + [ + (k.title(), v) + for k, v in resp.getheaders() + if not is_hop_by_hop_header(k) + ], + ) + + def read() -> t.Iterator[bytes]: + while True: + try: + data = resp.read(self.chunk_size) + except OSError: + break + + if not data: + break + + yield data + + return read() + + return application + + def __call__( + self, environ: "WSGIEnvironment", start_response: "StartResponse" + ) -> t.Iterable[bytes]: + path = environ["PATH_INFO"] + app = self.app + + for prefix, opts in self.targets.items(): + if path.startswith(prefix): + app = self.proxy_to(opts, path, prefix) + break + + return app(environ, start_response) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/lint.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/lint.py new file mode 100644 index 000000000..c74703b27 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/lint.py @@ -0,0 +1,420 @@ +""" +WSGI Protocol Linter +==================== + +This module provides a middleware that performs sanity checks on the +behavior of the WSGI server and application. It checks that the +:pep:`3333` WSGI spec is properly implemented. It also warns on some +common HTTP errors such as non-empty responses for 304 status codes. + +.. autoclass:: LintMiddleware + +:copyright: 2007 Pallets +:license: BSD-3-Clause +""" +import typing as t +from types import TracebackType +from urllib.parse import urlparse +from warnings import warn + +from ..datastructures import Headers +from ..http import is_entity_header +from ..wsgi import FileWrapper + +if t.TYPE_CHECKING: + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + + +class WSGIWarning(Warning): + """Warning class for WSGI warnings.""" + + +class HTTPWarning(Warning): + """Warning class for HTTP warnings.""" + + +def check_type(context: str, obj: object, need: t.Type = str) -> None: + if type(obj) is not need: + warn( + f"{context!r} requires {need.__name__!r}, got {type(obj).__name__!r}.", + WSGIWarning, + stacklevel=3, + ) + + +class InputStream: + def __init__(self, stream: t.IO[bytes]) -> None: + self._stream = stream + + def read(self, *args: t.Any) -> bytes: + if len(args) == 0: + warn( + "WSGI does not guarantee an EOF marker on the input stream, thus making" + " calls to 'wsgi.input.read()' unsafe. Conforming servers may never" + " return from this call.", + WSGIWarning, + stacklevel=2, + ) + elif len(args) != 1: + warn( + "Too many parameters passed to 'wsgi.input.read()'.", + WSGIWarning, + stacklevel=2, + ) + return self._stream.read(*args) + + def readline(self, *args: t.Any) -> bytes: + if len(args) == 0: + warn( + "Calls to 'wsgi.input.readline()' without arguments are unsafe. Use" + " 'wsgi.input.read()' instead.", + WSGIWarning, + stacklevel=2, + ) + elif len(args) == 1: + warn( + "'wsgi.input.readline()' was called with a size hint. WSGI does not" + " support this, although it's available on all major servers.", + WSGIWarning, + stacklevel=2, + ) + else: + raise TypeError("Too many arguments passed to 'wsgi.input.readline()'.") + return self._stream.readline(*args) + + def __iter__(self) -> t.Iterator[bytes]: + try: + return iter(self._stream) + except TypeError: + warn("'wsgi.input' is not iterable.", WSGIWarning, stacklevel=2) + return iter(()) + + def close(self) -> None: + warn("The application closed the input stream!", WSGIWarning, stacklevel=2) + self._stream.close() + + +class ErrorStream: + def __init__(self, stream: t.IO[str]) -> None: + self._stream = stream + + def write(self, s: str) -> None: + check_type("wsgi.error.write()", s, str) + self._stream.write(s) + + def flush(self) -> None: + self._stream.flush() + + def writelines(self, seq: t.Iterable[str]) -> None: + for line in seq: + self.write(line) + + def close(self) -> None: + warn("The application closed the error stream!", WSGIWarning, stacklevel=2) + self._stream.close() + + +class GuardedWrite: + def __init__(self, write: t.Callable[[bytes], None], chunks: t.List[int]) -> None: + self._write = write + self._chunks = chunks + + def __call__(self, s: bytes) -> None: + check_type("write()", s, bytes) + self._write(s) + self._chunks.append(len(s)) + + +class GuardedIterator: + def __init__( + self, + iterator: t.Iterable[bytes], + headers_set: t.Tuple[int, Headers], + chunks: t.List[int], + ) -> None: + self._iterator = iterator + self._next = iter(iterator).__next__ + self.closed = False + self.headers_set = headers_set + self.chunks = chunks + + def __iter__(self) -> "GuardedIterator": + return self + + def __next__(self) -> bytes: + if self.closed: + warn("Iterated over closed 'app_iter'.", WSGIWarning, stacklevel=2) + + rv = self._next() + + if not self.headers_set: + warn( + "The application returned before it started the response.", + WSGIWarning, + stacklevel=2, + ) + + check_type("application iterator items", rv, bytes) + self.chunks.append(len(rv)) + return rv + + def close(self) -> None: + self.closed = True + + if hasattr(self._iterator, "close"): + self._iterator.close() # type: ignore + + if self.headers_set: + status_code, headers = self.headers_set + bytes_sent = sum(self.chunks) + content_length = headers.get("content-length", type=int) + + if status_code == 304: + for key, _value in headers: + key = key.lower() + if key not in ("expires", "content-location") and is_entity_header( + key + ): + warn( + f"Entity header {key!r} found in 304 response.", HTTPWarning + ) + if bytes_sent: + warn("304 responses must not have a body.", HTTPWarning) + elif 100 <= status_code < 200 or status_code == 204: + if content_length != 0: + warn( + f"{status_code} responses must have an empty content length.", + HTTPWarning, + ) + if bytes_sent: + warn(f"{status_code} responses must not have a body.", HTTPWarning) + elif content_length is not None and content_length != bytes_sent: + warn( + "Content-Length and the number of bytes sent to the" + " client do not match.", + WSGIWarning, + ) + + def __del__(self) -> None: + if not self.closed: + try: + warn( + "Iterator was garbage collected before it was closed.", WSGIWarning + ) + except Exception: + pass + + +class LintMiddleware: + """Warns about common errors in the WSGI and HTTP behavior of the + server and wrapped application. Some of the issues it checks are: + + - invalid status codes + - non-bytes sent to the WSGI server + - strings returned from the WSGI application + - non-empty conditional responses + - unquoted etags + - relative URLs in the Location header + - unsafe calls to wsgi.input + - unclosed iterators + + Error information is emitted using the :mod:`warnings` module. + + :param app: The WSGI application to wrap. + + .. code-block:: python + + from werkzeug.middleware.lint import LintMiddleware + app = LintMiddleware(app) + """ + + def __init__(self, app: "WSGIApplication") -> None: + self.app = app + + def check_environ(self, environ: "WSGIEnvironment") -> None: + if type(environ) is not dict: + warn( + "WSGI environment is not a standard Python dict.", + WSGIWarning, + stacklevel=4, + ) + for key in ( + "REQUEST_METHOD", + "SERVER_NAME", + "SERVER_PORT", + "wsgi.version", + "wsgi.input", + "wsgi.errors", + "wsgi.multithread", + "wsgi.multiprocess", + "wsgi.run_once", + ): + if key not in environ: + warn( + f"Required environment key {key!r} not found", + WSGIWarning, + stacklevel=3, + ) + if environ["wsgi.version"] != (1, 0): + warn("Environ is not a WSGI 1.0 environ.", WSGIWarning, stacklevel=3) + + script_name = environ.get("SCRIPT_NAME", "") + path_info = environ.get("PATH_INFO", "") + + if script_name and script_name[0] != "/": + warn( + f"'SCRIPT_NAME' does not start with a slash: {script_name!r}", + WSGIWarning, + stacklevel=3, + ) + + if path_info and path_info[0] != "/": + warn( + f"'PATH_INFO' does not start with a slash: {path_info!r}", + WSGIWarning, + stacklevel=3, + ) + + def check_start_response( + self, + status: str, + headers: t.List[t.Tuple[str, str]], + exc_info: t.Optional[ + t.Tuple[t.Type[BaseException], BaseException, TracebackType] + ], + ) -> t.Tuple[int, Headers]: + check_type("status", status, str) + status_code_str = status.split(None, 1)[0] + + if len(status_code_str) != 3 or not status_code_str.isdigit(): + warn("Status code must be three digits.", WSGIWarning, stacklevel=3) + + if len(status) < 4 or status[3] != " ": + warn( + f"Invalid value for status {status!r}. Valid status strings are three" + " digits, a space and a status explanation.", + WSGIWarning, + stacklevel=3, + ) + + status_code = int(status_code_str) + + if status_code < 100: + warn("Status code < 100 detected.", WSGIWarning, stacklevel=3) + + if type(headers) is not list: + warn("Header list is not a list.", WSGIWarning, stacklevel=3) + + for item in headers: + if type(item) is not tuple or len(item) != 2: + warn("Header items must be 2-item tuples.", WSGIWarning, stacklevel=3) + name, value = item + if type(name) is not str or type(value) is not str: + warn( + "Header keys and values must be strings.", WSGIWarning, stacklevel=3 + ) + if name.lower() == "status": + warn( + "The status header is not supported due to" + " conflicts with the CGI spec.", + WSGIWarning, + stacklevel=3, + ) + + if exc_info is not None and not isinstance(exc_info, tuple): + warn("Invalid value for exc_info.", WSGIWarning, stacklevel=3) + + headers = Headers(headers) + self.check_headers(headers) + + return status_code, headers + + def check_headers(self, headers: Headers) -> None: + etag = headers.get("etag") + + if etag is not None: + if etag.startswith(("W/", "w/")): + if etag.startswith("w/"): + warn( + "Weak etag indicator should be upper case.", + HTTPWarning, + stacklevel=4, + ) + + etag = etag[2:] + + if not (etag[:1] == etag[-1:] == '"'): + warn("Unquoted etag emitted.", HTTPWarning, stacklevel=4) + + location = headers.get("location") + + if location is not None: + if not urlparse(location).netloc: + warn( + "Absolute URLs required for location header.", + HTTPWarning, + stacklevel=4, + ) + + def check_iterator(self, app_iter: t.Iterable[bytes]) -> None: + if isinstance(app_iter, bytes): + warn( + "The application returned a bytestring. The response will send one" + " character at a time to the client, which will kill performance." + " Return a list or iterable instead.", + WSGIWarning, + stacklevel=3, + ) + + def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Iterable[bytes]: + if len(args) != 2: + warn("A WSGI app takes two arguments.", WSGIWarning, stacklevel=2) + + if kwargs: + warn( + "A WSGI app does not take keyword arguments.", WSGIWarning, stacklevel=2 + ) + + environ: "WSGIEnvironment" = args[0] + start_response: "StartResponse" = args[1] + + self.check_environ(environ) + environ["wsgi.input"] = InputStream(environ["wsgi.input"]) + environ["wsgi.errors"] = ErrorStream(environ["wsgi.errors"]) + + # Hook our own file wrapper in so that applications will always + # iterate to the end and we can check the content length. + environ["wsgi.file_wrapper"] = FileWrapper + + headers_set: t.List[t.Any] = [] + chunks: t.List[int] = [] + + def checking_start_response( + *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[bytes], None]: + if len(args) not in {2, 3}: + warn( + f"Invalid number of arguments: {len(args)}, expected 2 or 3.", + WSGIWarning, + stacklevel=2, + ) + + if kwargs: + warn("'start_response' does not take keyword arguments.", WSGIWarning) + + status: str = args[0] + headers: t.List[t.Tuple[str, str]] = args[1] + exc_info: t.Optional[ + t.Tuple[t.Type[BaseException], BaseException, TracebackType] + ] = (args[2] if len(args) == 3 else None) + + headers_set[:] = self.check_start_response(status, headers, exc_info) + return GuardedWrite(start_response(status, headers, exc_info), chunks) + + app_iter = self.app(environ, t.cast("StartResponse", checking_start_response)) + self.check_iterator(app_iter) + return GuardedIterator( + app_iter, t.cast(t.Tuple[int, Headers], headers_set), chunks + ) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/profiler.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/profiler.py new file mode 100644 index 000000000..200dae034 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/profiler.py @@ -0,0 +1,139 @@ +""" +Application Profiler +==================== + +This module provides a middleware that profiles each request with the +:mod:`cProfile` module. This can help identify bottlenecks in your code +that may be slowing down your application. + +.. autoclass:: ProfilerMiddleware + +:copyright: 2007 Pallets +:license: BSD-3-Clause +""" +import os.path +import sys +import time +import typing as t +from pstats import Stats + +try: + from cProfile import Profile +except ImportError: + from profile import Profile # type: ignore + +if t.TYPE_CHECKING: + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + + +class ProfilerMiddleware: + """Wrap a WSGI application and profile the execution of each + request. Responses are buffered so that timings are more exact. + + If ``stream`` is given, :class:`pstats.Stats` are written to it + after each request. If ``profile_dir`` is given, :mod:`cProfile` + data files are saved to that directory, one file per request. + + The filename can be customized by passing ``filename_format``. If + it is a string, it will be formatted using :meth:`str.format` with + the following fields available: + + - ``{method}`` - The request method; GET, POST, etc. + - ``{path}`` - The request path or 'root' should one not exist. + - ``{elapsed}`` - The elapsed time of the request. + - ``{time}`` - The time of the request. + + If it is a callable, it will be called with the WSGI ``environ`` + dict and should return a filename. + + :param app: The WSGI application to wrap. + :param stream: Write stats to this stream. Disable with ``None``. + :param sort_by: A tuple of columns to sort stats by. See + :meth:`pstats.Stats.sort_stats`. + :param restrictions: A tuple of restrictions to filter stats by. See + :meth:`pstats.Stats.print_stats`. + :param profile_dir: Save profile data files to this directory. + :param filename_format: Format string for profile data file names, + or a callable returning a name. See explanation above. + + .. code-block:: python + + from werkzeug.middleware.profiler import ProfilerMiddleware + app = ProfilerMiddleware(app) + + .. versionchanged:: 0.15 + Stats are written even if ``profile_dir`` is given, and can be + disable by passing ``stream=None``. + + .. versionadded:: 0.15 + Added ``filename_format``. + + .. versionadded:: 0.9 + Added ``restrictions`` and ``profile_dir``. + """ + + def __init__( + self, + app: "WSGIApplication", + stream: t.IO[str] = sys.stdout, + sort_by: t.Iterable[str] = ("time", "calls"), + restrictions: t.Iterable[t.Union[str, int, float]] = (), + profile_dir: t.Optional[str] = None, + filename_format: str = "{method}.{path}.{elapsed:.0f}ms.{time:.0f}.prof", + ) -> None: + self._app = app + self._stream = stream + self._sort_by = sort_by + self._restrictions = restrictions + self._profile_dir = profile_dir + self._filename_format = filename_format + + def __call__( + self, environ: "WSGIEnvironment", start_response: "StartResponse" + ) -> t.Iterable[bytes]: + response_body: t.List[bytes] = [] + + def catching_start_response(status, headers, exc_info=None): # type: ignore + start_response(status, headers, exc_info) + return response_body.append + + def runapp() -> None: + app_iter = self._app( + environ, t.cast("StartResponse", catching_start_response) + ) + response_body.extend(app_iter) + + if hasattr(app_iter, "close"): + app_iter.close() # type: ignore + + profile = Profile() + start = time.time() + profile.runcall(runapp) + body = b"".join(response_body) + elapsed = time.time() - start + + if self._profile_dir is not None: + if callable(self._filename_format): + filename = self._filename_format(environ) + else: + filename = self._filename_format.format( + method=environ["REQUEST_METHOD"], + path=environ["PATH_INFO"].strip("/").replace("/", ".") or "root", + elapsed=elapsed * 1000.0, + time=time.time(), + ) + filename = os.path.join(self._profile_dir, filename) + profile.dump_stats(filename) + + if self._stream is not None: + stats = Stats(profile, stream=self._stream) + stats.sort_stats(*self._sort_by) + print("-" * 80, file=self._stream) + path_info = environ.get("PATH_INFO", "") + print(f"PATH: {path_info!r}", file=self._stream) + stats.print_stats(*self._restrictions) + print(f"{'-' * 80}\n", file=self._stream) + + return [body] diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/proxy_fix.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/proxy_fix.py new file mode 100644 index 000000000..e90b1b34e --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/proxy_fix.py @@ -0,0 +1,187 @@ +""" +X-Forwarded-For Proxy Fix +========================= + +This module provides a middleware that adjusts the WSGI environ based on +``X-Forwarded-`` headers that proxies in front of an application may +set. + +When an application is running behind a proxy server, WSGI may see the +request as coming from that server rather than the real client. Proxies +set various headers to track where the request actually came from. + +This middleware should only be used if the application is actually +behind such a proxy, and should be configured with the number of proxies +that are chained in front of it. Not all proxies set all the headers. +Since incoming headers can be faked, you must set how many proxies are +setting each header so the middleware knows what to trust. + +.. autoclass:: ProxyFix + +:copyright: 2007 Pallets +:license: BSD-3-Clause +""" +import typing as t + +from ..http import parse_list_header + +if t.TYPE_CHECKING: + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + + +class ProxyFix: + """Adjust the WSGI environ based on ``X-Forwarded-`` that proxies in + front of the application may set. + + - ``X-Forwarded-For`` sets ``REMOTE_ADDR``. + - ``X-Forwarded-Proto`` sets ``wsgi.url_scheme``. + - ``X-Forwarded-Host`` sets ``HTTP_HOST``, ``SERVER_NAME``, and + ``SERVER_PORT``. + - ``X-Forwarded-Port`` sets ``HTTP_HOST`` and ``SERVER_PORT``. + - ``X-Forwarded-Prefix`` sets ``SCRIPT_NAME``. + + You must tell the middleware how many proxies set each header so it + knows what values to trust. It is a security issue to trust values + that came from the client rather than a proxy. + + The original values of the headers are stored in the WSGI + environ as ``werkzeug.proxy_fix.orig``, a dict. + + :param app: The WSGI application to wrap. + :param x_for: Number of values to trust for ``X-Forwarded-For``. + :param x_proto: Number of values to trust for ``X-Forwarded-Proto``. + :param x_host: Number of values to trust for ``X-Forwarded-Host``. + :param x_port: Number of values to trust for ``X-Forwarded-Port``. + :param x_prefix: Number of values to trust for + ``X-Forwarded-Prefix``. + + .. code-block:: python + + from werkzeug.middleware.proxy_fix import ProxyFix + # App is behind one proxy that sets the -For and -Host headers. + app = ProxyFix(app, x_for=1, x_host=1) + + .. versionchanged:: 1.0 + Deprecated code has been removed: + + * The ``num_proxies`` argument and attribute. + * The ``get_remote_addr`` method. + * The environ keys ``orig_remote_addr``, + ``orig_wsgi_url_scheme``, and ``orig_http_host``. + + .. versionchanged:: 0.15 + All headers support multiple values. The ``num_proxies`` + argument is deprecated. Each header is configured with a + separate number of trusted proxies. + + .. versionchanged:: 0.15 + Original WSGI environ values are stored in the + ``werkzeug.proxy_fix.orig`` dict. ``orig_remote_addr``, + ``orig_wsgi_url_scheme``, and ``orig_http_host`` are deprecated + and will be removed in 1.0. + + .. versionchanged:: 0.15 + Support ``X-Forwarded-Port`` and ``X-Forwarded-Prefix``. + + .. versionchanged:: 0.15 + ``X-Forwarded-Host`` and ``X-Forwarded-Port`` modify + ``SERVER_NAME`` and ``SERVER_PORT``. + """ + + def __init__( + self, + app: "WSGIApplication", + x_for: int = 1, + x_proto: int = 1, + x_host: int = 0, + x_port: int = 0, + x_prefix: int = 0, + ) -> None: + self.app = app + self.x_for = x_for + self.x_proto = x_proto + self.x_host = x_host + self.x_port = x_port + self.x_prefix = x_prefix + + def _get_real_value(self, trusted: int, value: t.Optional[str]) -> t.Optional[str]: + """Get the real value from a list header based on the configured + number of trusted proxies. + + :param trusted: Number of values to trust in the header. + :param value: Comma separated list header value to parse. + :return: The real value, or ``None`` if there are fewer values + than the number of trusted proxies. + + .. versionchanged:: 1.0 + Renamed from ``_get_trusted_comma``. + + .. versionadded:: 0.15 + """ + if not (trusted and value): + return None + values = parse_list_header(value) + if len(values) >= trusted: + return values[-trusted] + return None + + def __call__( + self, environ: "WSGIEnvironment", start_response: "StartResponse" + ) -> t.Iterable[bytes]: + """Modify the WSGI environ based on the various ``Forwarded`` + headers before calling the wrapped application. Store the + original environ values in ``werkzeug.proxy_fix.orig_{key}``. + """ + environ_get = environ.get + orig_remote_addr = environ_get("REMOTE_ADDR") + orig_wsgi_url_scheme = environ_get("wsgi.url_scheme") + orig_http_host = environ_get("HTTP_HOST") + environ.update( + { + "werkzeug.proxy_fix.orig": { + "REMOTE_ADDR": orig_remote_addr, + "wsgi.url_scheme": orig_wsgi_url_scheme, + "HTTP_HOST": orig_http_host, + "SERVER_NAME": environ_get("SERVER_NAME"), + "SERVER_PORT": environ_get("SERVER_PORT"), + "SCRIPT_NAME": environ_get("SCRIPT_NAME"), + } + } + ) + + x_for = self._get_real_value(self.x_for, environ_get("HTTP_X_FORWARDED_FOR")) + if x_for: + environ["REMOTE_ADDR"] = x_for + + x_proto = self._get_real_value( + self.x_proto, environ_get("HTTP_X_FORWARDED_PROTO") + ) + if x_proto: + environ["wsgi.url_scheme"] = x_proto + + x_host = self._get_real_value(self.x_host, environ_get("HTTP_X_FORWARDED_HOST")) + if x_host: + environ["HTTP_HOST"] = x_host + parts = x_host.split(":", 1) + environ["SERVER_NAME"] = parts[0] + if len(parts) == 2: + environ["SERVER_PORT"] = parts[1] + + x_port = self._get_real_value(self.x_port, environ_get("HTTP_X_FORWARDED_PORT")) + if x_port: + host = environ.get("HTTP_HOST") + if host: + parts = host.split(":", 1) + host = parts[0] if len(parts) == 2 else host + environ["HTTP_HOST"] = f"{host}:{x_port}" + environ["SERVER_PORT"] = x_port + + x_prefix = self._get_real_value( + self.x_prefix, environ_get("HTTP_X_FORWARDED_PREFIX") + ) + if x_prefix: + environ["SCRIPT_NAME"] = x_prefix + + return self.app(environ, start_response) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/shared_data.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/shared_data.py new file mode 100644 index 000000000..62da67277 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/middleware/shared_data.py @@ -0,0 +1,320 @@ +""" +Serve Shared Static Files +========================= + +.. autoclass:: SharedDataMiddleware + :members: is_allowed + +:copyright: 2007 Pallets +:license: BSD-3-Clause +""" +import mimetypes +import os +import pkgutil +import posixpath +import typing as t +from datetime import datetime +from datetime import timezone +from io import BytesIO +from time import time +from zlib import adler32 + +from ..filesystem import get_filesystem_encoding +from ..http import http_date +from ..http import is_resource_modified +from ..security import safe_join +from ..utils import get_content_type +from ..wsgi import get_path_info +from ..wsgi import wrap_file + +_TOpener = t.Callable[[], t.Tuple[t.IO[bytes], datetime, int]] +_TLoader = t.Callable[[t.Optional[str]], t.Tuple[t.Optional[str], t.Optional[_TOpener]]] + +if t.TYPE_CHECKING: + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + + +class SharedDataMiddleware: + + """A WSGI middleware which provides static content for development + environments or simple server setups. Its usage is quite simple:: + + import os + from werkzeug.middleware.shared_data import SharedDataMiddleware + + app = SharedDataMiddleware(app, { + '/shared': os.path.join(os.path.dirname(__file__), 'shared') + }) + + The contents of the folder ``./shared`` will now be available on + ``http://example.com/shared/``. This is pretty useful during development + because a standalone media server is not required. Files can also be + mounted on the root folder and still continue to use the application because + the shared data middleware forwards all unhandled requests to the + application, even if the requests are below one of the shared folders. + + If `pkg_resources` is available you can also tell the middleware to serve + files from package data:: + + app = SharedDataMiddleware(app, { + '/static': ('myapplication', 'static') + }) + + This will then serve the ``static`` folder in the `myapplication` + Python package. + + The optional `disallow` parameter can be a list of :func:`~fnmatch.fnmatch` + rules for files that are not accessible from the web. If `cache` is set to + `False` no caching headers are sent. + + Currently the middleware does not support non-ASCII filenames. If the + encoding on the file system happens to match the encoding of the URI it may + work but this could also be by accident. We strongly suggest using ASCII + only file names for static files. + + The middleware will guess the mimetype using the Python `mimetype` + module. If it's unable to figure out the charset it will fall back + to `fallback_mimetype`. + + :param app: the application to wrap. If you don't want to wrap an + application you can pass it :exc:`NotFound`. + :param exports: a list or dict of exported files and folders. + :param disallow: a list of :func:`~fnmatch.fnmatch` rules. + :param cache: enable or disable caching headers. + :param cache_timeout: the cache timeout in seconds for the headers. + :param fallback_mimetype: The fallback mimetype for unknown files. + + .. versionchanged:: 1.0 + The default ``fallback_mimetype`` is + ``application/octet-stream``. If a filename looks like a text + mimetype, the ``utf-8`` charset is added to it. + + .. versionadded:: 0.6 + Added ``fallback_mimetype``. + + .. versionchanged:: 0.5 + Added ``cache_timeout``. + """ + + def __init__( + self, + app: "WSGIApplication", + exports: t.Union[ + t.Dict[str, t.Union[str, t.Tuple[str, str]]], + t.Iterable[t.Tuple[str, t.Union[str, t.Tuple[str, str]]]], + ], + disallow: None = None, + cache: bool = True, + cache_timeout: int = 60 * 60 * 12, + fallback_mimetype: str = "application/octet-stream", + ) -> None: + self.app = app + self.exports: t.List[t.Tuple[str, _TLoader]] = [] + self.cache = cache + self.cache_timeout = cache_timeout + + if isinstance(exports, dict): + exports = exports.items() + + for key, value in exports: + if isinstance(value, tuple): + loader = self.get_package_loader(*value) + elif isinstance(value, str): + if os.path.isfile(value): + loader = self.get_file_loader(value) + else: + loader = self.get_directory_loader(value) + else: + raise TypeError(f"unknown def {value!r}") + + self.exports.append((key, loader)) + + if disallow is not None: + from fnmatch import fnmatch + + self.is_allowed = lambda x: not fnmatch(x, disallow) + + self.fallback_mimetype = fallback_mimetype + + def is_allowed(self, filename: str) -> bool: + """Subclasses can override this method to disallow the access to + certain files. However by providing `disallow` in the constructor + this method is overwritten. + """ + return True + + def _opener(self, filename: str) -> _TOpener: + return lambda: ( + open(filename, "rb"), + datetime.fromtimestamp(os.path.getmtime(filename), tz=timezone.utc), + int(os.path.getsize(filename)), + ) + + def get_file_loader(self, filename: str) -> _TLoader: + return lambda x: (os.path.basename(filename), self._opener(filename)) + + def get_package_loader(self, package: str, package_path: str) -> _TLoader: + load_time = datetime.now(timezone.utc) + provider = pkgutil.get_loader(package) + + if hasattr(provider, "get_resource_reader"): + # Python 3 + reader = provider.get_resource_reader(package) # type: ignore + + def loader( + path: t.Optional[str], + ) -> t.Tuple[t.Optional[str], t.Optional[_TOpener]]: + if path is None: + return None, None + + path = safe_join(package_path, path) + + if path is None: + return None, None + + basename = posixpath.basename(path) + + try: + resource = reader.open_resource(path) + except OSError: + return None, None + + if isinstance(resource, BytesIO): + return ( + basename, + lambda: (resource, load_time, len(resource.getvalue())), + ) + + return ( + basename, + lambda: ( + resource, + datetime.fromtimestamp( + os.path.getmtime(resource.name), tz=timezone.utc + ), + os.path.getsize(resource.name), + ), + ) + + else: + # Python 3.6 + package_filename = provider.get_filename(package) # type: ignore + is_filesystem = os.path.exists(package_filename) + root = os.path.join(os.path.dirname(package_filename), package_path) + + def loader( + path: t.Optional[str], + ) -> t.Tuple[t.Optional[str], t.Optional[_TOpener]]: + if path is None: + return None, None + + path = safe_join(root, path) + + if path is None: + return None, None + + basename = posixpath.basename(path) + + if is_filesystem: + if not os.path.isfile(path): + return None, None + + return basename, self._opener(path) + + try: + data = provider.get_data(path) # type: ignore + except OSError: + return None, None + + return basename, lambda: (BytesIO(data), load_time, len(data)) + + return loader + + def get_directory_loader(self, directory: str) -> _TLoader: + def loader( + path: t.Optional[str], + ) -> t.Tuple[t.Optional[str], t.Optional[_TOpener]]: + if path is not None: + path = safe_join(directory, path) + + if path is None: + return None, None + else: + path = directory + + if os.path.isfile(path): + return os.path.basename(path), self._opener(path) + + return None, None + + return loader + + def generate_etag(self, mtime: datetime, file_size: int, real_filename: str) -> str: + if not isinstance(real_filename, bytes): + real_filename = real_filename.encode( # type: ignore + get_filesystem_encoding() + ) + + timestamp = mtime.timestamp() + checksum = adler32(real_filename) & 0xFFFFFFFF # type: ignore + return f"wzsdm-{timestamp}-{file_size}-{checksum}" + + def __call__( + self, environ: "WSGIEnvironment", start_response: "StartResponse" + ) -> t.Iterable[bytes]: + path = get_path_info(environ) + file_loader = None + + for search_path, loader in self.exports: + if search_path == path: + real_filename, file_loader = loader(None) + + if file_loader is not None: + break + + if not search_path.endswith("/"): + search_path += "/" + + if path.startswith(search_path): + real_filename, file_loader = loader(path[len(search_path) :]) + + if file_loader is not None: + break + + if file_loader is None or not self.is_allowed(real_filename): # type: ignore + return self.app(environ, start_response) + + guessed_type = mimetypes.guess_type(real_filename) # type: ignore + mime_type = get_content_type(guessed_type[0] or self.fallback_mimetype, "utf-8") + f, mtime, file_size = file_loader() + + headers = [("Date", http_date())] + + if self.cache: + timeout = self.cache_timeout + etag = self.generate_etag(mtime, file_size, real_filename) # type: ignore + headers += [ + ("Etag", f'"{etag}"'), + ("Cache-Control", f"max-age={timeout}, public"), + ] + + if not is_resource_modified(environ, etag, last_modified=mtime): + f.close() + start_response("304 Not Modified", headers) + return [] + + headers.append(("Expires", http_date(time() + timeout))) + else: + headers.append(("Cache-Control", "public")) + + headers.extend( + ( + ("Content-Type", mime_type), + ("Content-Length", str(file_size)), + ("Last-Modified", http_date(mtime)), + ) + ) + start_response("200 OK", headers) + return wrap_file(environ, f) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/py.typed b/flask-app/venv/lib/python3.8/site-packages/werkzeug/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/routing.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/routing.py new file mode 100644 index 000000000..ddb8ef9c1 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/routing.py @@ -0,0 +1,2341 @@ +"""When it comes to combining multiple controller or view functions +(however you want to call them) you need a dispatcher. A simple way +would be applying regular expression tests on the ``PATH_INFO`` and +calling registered callback functions that return the value then. + +This module implements a much more powerful system than simple regular +expression matching because it can also convert values in the URLs and +build URLs. + +Here a simple example that creates a URL map for an application with +two subdomains (www and kb) and some URL rules: + +.. code-block:: python + + m = Map([ + # Static URLs + Rule('/', endpoint='static/index'), + Rule('/about', endpoint='static/about'), + Rule('/help', endpoint='static/help'), + # Knowledge Base + Subdomain('kb', [ + Rule('/', endpoint='kb/index'), + Rule('/browse/', endpoint='kb/browse'), + Rule('/browse//', endpoint='kb/browse'), + Rule('/browse//', endpoint='kb/browse') + ]) + ], default_subdomain='www') + +If the application doesn't use subdomains it's perfectly fine to not set +the default subdomain and not use the `Subdomain` rule factory. The +endpoint in the rules can be anything, for example import paths or +unique identifiers. The WSGI application can use those endpoints to get the +handler for that URL. It doesn't have to be a string at all but it's +recommended. + +Now it's possible to create a URL adapter for one of the subdomains and +build URLs: + +.. code-block:: python + + c = m.bind('example.com') + + c.build("kb/browse", dict(id=42)) + 'http://kb.example.com/browse/42/' + + c.build("kb/browse", dict()) + 'http://kb.example.com/browse/' + + c.build("kb/browse", dict(id=42, page=3)) + 'http://kb.example.com/browse/42/3' + + c.build("static/about") + '/about' + + c.build("static/index", force_external=True) + 'http://www.example.com/' + + c = m.bind('example.com', subdomain='kb') + + c.build("static/about") + 'http://www.example.com/about' + +The first argument to bind is the server name *without* the subdomain. +Per default it will assume that the script is mounted on the root, but +often that's not the case so you can provide the real mount point as +second argument: + +.. code-block:: python + + c = m.bind('example.com', '/applications/example') + +The third argument can be the subdomain, if not given the default +subdomain is used. For more details about binding have a look at the +documentation of the `MapAdapter`. + +And here is how you can match URLs: + +.. code-block:: python + + c = m.bind('example.com') + + c.match("/") + ('static/index', {}) + + c.match("/about") + ('static/about', {}) + + c = m.bind('example.com', '/', 'kb') + + c.match("/") + ('kb/index', {}) + + c.match("/browse/42/23") + ('kb/browse', {'id': 42, 'page': 23}) + +If matching fails you get a ``NotFound`` exception, if the rule thinks +it's a good idea to redirect (for example because the URL was defined +to have a slash at the end but the request was missing that slash) it +will raise a ``RequestRedirect`` exception. Both are subclasses of +``HTTPException`` so you can use those errors as responses in the +application. + +If matching succeeded but the URL rule was incompatible to the given +method (for example there were only rules for ``GET`` and ``HEAD`` but +routing tried to match a ``POST`` request) a ``MethodNotAllowed`` +exception is raised. +""" +import ast +import difflib +import posixpath +import re +import typing +import typing as t +import uuid +import warnings +from pprint import pformat +from string import Template +from threading import Lock +from types import CodeType + +from ._internal import _encode_idna +from ._internal import _get_environ +from ._internal import _to_bytes +from ._internal import _to_str +from ._internal import _wsgi_decoding_dance +from .datastructures import ImmutableDict +from .datastructures import MultiDict +from .exceptions import BadHost +from .exceptions import BadRequest +from .exceptions import HTTPException +from .exceptions import MethodNotAllowed +from .exceptions import NotFound +from .urls import _fast_url_quote +from .urls import url_encode +from .urls import url_join +from .urls import url_quote +from .urls import url_unquote +from .utils import cached_property +from .utils import redirect +from .wsgi import get_host + +if t.TYPE_CHECKING: + import typing_extensions as te + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + from .wrappers.response import Response + +_rule_re = re.compile( + r""" + (?P[^<]*) # static rule data + < + (?: + (?P[a-zA-Z_][a-zA-Z0-9_]*) # converter name + (?:\((?P.*?)\))? # converter arguments + \: # variable delimiter + )? + (?P[a-zA-Z_][a-zA-Z0-9_]*) # variable name + > + """, + re.VERBOSE, +) +_simple_rule_re = re.compile(r"<([^>]+)>") +_converter_args_re = re.compile( + r""" + ((?P\w+)\s*=\s*)? + (?P + True|False| + \d+.\d+| + \d+.| + \d+| + [\w\d_.]+| + [urUR]?(?P"[^"]*?"|'[^']*') + )\s*, + """, + re.VERBOSE, +) + + +_PYTHON_CONSTANTS = {"None": None, "True": True, "False": False} + + +def _pythonize(value: str) -> t.Union[None, bool, int, float, str]: + if value in _PYTHON_CONSTANTS: + return _PYTHON_CONSTANTS[value] + for convert in int, float: + try: + return convert(value) # type: ignore + except ValueError: + pass + if value[:1] == value[-1:] and value[0] in "\"'": + value = value[1:-1] + return str(value) + + +def parse_converter_args(argstr: str) -> t.Tuple[t.Tuple, t.Dict[str, t.Any]]: + argstr += "," + args = [] + kwargs = {} + + for item in _converter_args_re.finditer(argstr): + value = item.group("stringval") + if value is None: + value = item.group("value") + value = _pythonize(value) + if not item.group("name"): + args.append(value) + else: + name = item.group("name") + kwargs[name] = value + + return tuple(args), kwargs + + +def parse_rule(rule: str) -> t.Iterator[t.Tuple[t.Optional[str], t.Optional[str], str]]: + """Parse a rule and return it as generator. Each iteration yields tuples + in the form ``(converter, arguments, variable)``. If the converter is + `None` it's a static url part, otherwise it's a dynamic one. + + :internal: + """ + pos = 0 + end = len(rule) + do_match = _rule_re.match + used_names = set() + while pos < end: + m = do_match(rule, pos) + if m is None: + break + data = m.groupdict() + if data["static"]: + yield None, None, data["static"] + variable = data["variable"] + converter = data["converter"] or "default" + if variable in used_names: + raise ValueError(f"variable name {variable!r} used twice.") + used_names.add(variable) + yield converter, data["args"] or None, variable + pos = m.end() + if pos < end: + remaining = rule[pos:] + if ">" in remaining or "<" in remaining: + raise ValueError(f"malformed url rule: {rule!r}") + yield None, None, remaining + + +class RoutingException(Exception): + """Special exceptions that require the application to redirect, notifying + about missing urls, etc. + + :internal: + """ + + +class RequestRedirect(HTTPException, RoutingException): + """Raise if the map requests a redirect. This is for example the case if + `strict_slashes` are activated and an url that requires a trailing slash. + + The attribute `new_url` contains the absolute destination url. + """ + + code = 308 + + def __init__(self, new_url: str) -> None: + super().__init__(new_url) + self.new_url = new_url + + def get_response( + self, + environ: t.Optional["WSGIEnvironment"] = None, + scope: t.Optional[dict] = None, + ) -> "Response": + return redirect(self.new_url, self.code) + + +class RequestPath(RoutingException): + """Internal exception.""" + + __slots__ = ("path_info",) + + def __init__(self, path_info: str) -> None: + super().__init__() + self.path_info = path_info + + +class RequestAliasRedirect(RoutingException): # noqa: B903 + """This rule is an alias and wants to redirect to the canonical URL.""" + + def __init__(self, matched_values: t.Mapping[str, t.Any]) -> None: + super().__init__() + self.matched_values = matched_values + + +class BuildError(RoutingException, LookupError): + """Raised if the build system cannot find a URL for an endpoint with the + values provided. + """ + + def __init__( + self, + endpoint: str, + values: t.Mapping[str, t.Any], + method: t.Optional[str], + adapter: t.Optional["MapAdapter"] = None, + ) -> None: + super().__init__(endpoint, values, method) + self.endpoint = endpoint + self.values = values + self.method = method + self.adapter = adapter + + @cached_property + def suggested(self) -> t.Optional["Rule"]: + return self.closest_rule(self.adapter) + + def closest_rule(self, adapter: t.Optional["MapAdapter"]) -> t.Optional["Rule"]: + def _score_rule(rule: "Rule") -> float: + return sum( + [ + 0.98 + * difflib.SequenceMatcher( + None, rule.endpoint, self.endpoint + ).ratio(), + 0.01 * bool(set(self.values or ()).issubset(rule.arguments)), + 0.01 * bool(rule.methods and self.method in rule.methods), + ] + ) + + if adapter and adapter.map._rules: + return max(adapter.map._rules, key=_score_rule) + + return None + + def __str__(self) -> str: + message = [f"Could not build url for endpoint {self.endpoint!r}"] + if self.method: + message.append(f" ({self.method!r})") + if self.values: + message.append(f" with values {sorted(self.values)!r}") + message.append(".") + if self.suggested: + if self.endpoint == self.suggested.endpoint: + if ( + self.method + and self.suggested.methods is not None + and self.method not in self.suggested.methods + ): + message.append( + " Did you mean to use methods" + f" {sorted(self.suggested.methods)!r}?" + ) + missing_values = self.suggested.arguments.union( + set(self.suggested.defaults or ()) + ) - set(self.values.keys()) + if missing_values: + message.append( + f" Did you forget to specify values {sorted(missing_values)!r}?" + ) + else: + message.append(f" Did you mean {self.suggested.endpoint!r} instead?") + return "".join(message) + + +class WebsocketMismatch(BadRequest): + """The only matched rule is either a WebSocket and the request is + HTTP, or the rule is HTTP and the request is a WebSocket. + """ + + +class ValidationError(ValueError): + """Validation error. If a rule converter raises this exception the rule + does not match the current URL and the next URL is tried. + """ + + +class RuleFactory: + """As soon as you have more complex URL setups it's a good idea to use rule + factories to avoid repetitive tasks. Some of them are builtin, others can + be added by subclassing `RuleFactory` and overriding `get_rules`. + """ + + def get_rules(self, map: "Map") -> t.Iterable["Rule"]: + """Subclasses of `RuleFactory` have to override this method and return + an iterable of rules.""" + raise NotImplementedError() + + +class Subdomain(RuleFactory): + """All URLs provided by this factory have the subdomain set to a + specific domain. For example if you want to use the subdomain for + the current language this can be a good setup:: + + url_map = Map([ + Rule('/', endpoint='#select_language'), + Subdomain('', [ + Rule('/', endpoint='index'), + Rule('/about', endpoint='about'), + Rule('/help', endpoint='help') + ]) + ]) + + All the rules except for the ``'#select_language'`` endpoint will now + listen on a two letter long subdomain that holds the language code + for the current request. + """ + + def __init__(self, subdomain: str, rules: t.Iterable[RuleFactory]) -> None: + self.subdomain = subdomain + self.rules = rules + + def get_rules(self, map: "Map") -> t.Iterator["Rule"]: + for rulefactory in self.rules: + for rule in rulefactory.get_rules(map): + rule = rule.empty() + rule.subdomain = self.subdomain + yield rule + + +class Submount(RuleFactory): + """Like `Subdomain` but prefixes the URL rule with a given string:: + + url_map = Map([ + Rule('/', endpoint='index'), + Submount('/blog', [ + Rule('/', endpoint='blog/index'), + Rule('/entry/', endpoint='blog/show') + ]) + ]) + + Now the rule ``'blog/show'`` matches ``/blog/entry/``. + """ + + def __init__(self, path: str, rules: t.Iterable[RuleFactory]) -> None: + self.path = path.rstrip("/") + self.rules = rules + + def get_rules(self, map: "Map") -> t.Iterator["Rule"]: + for rulefactory in self.rules: + for rule in rulefactory.get_rules(map): + rule = rule.empty() + rule.rule = self.path + rule.rule + yield rule + + +class EndpointPrefix(RuleFactory): + """Prefixes all endpoints (which must be strings for this factory) with + another string. This can be useful for sub applications:: + + url_map = Map([ + Rule('/', endpoint='index'), + EndpointPrefix('blog/', [Submount('/blog', [ + Rule('/', endpoint='index'), + Rule('/entry/', endpoint='show') + ])]) + ]) + """ + + def __init__(self, prefix: str, rules: t.Iterable[RuleFactory]) -> None: + self.prefix = prefix + self.rules = rules + + def get_rules(self, map: "Map") -> t.Iterator["Rule"]: + for rulefactory in self.rules: + for rule in rulefactory.get_rules(map): + rule = rule.empty() + rule.endpoint = self.prefix + rule.endpoint + yield rule + + +class RuleTemplate: + """Returns copies of the rules wrapped and expands string templates in + the endpoint, rule, defaults or subdomain sections. + + Here a small example for such a rule template:: + + from werkzeug.routing import Map, Rule, RuleTemplate + + resource = RuleTemplate([ + Rule('/$name/', endpoint='$name.list'), + Rule('/$name/', endpoint='$name.show') + ]) + + url_map = Map([resource(name='user'), resource(name='page')]) + + When a rule template is called the keyword arguments are used to + replace the placeholders in all the string parameters. + """ + + def __init__(self, rules: t.Iterable["Rule"]) -> None: + self.rules = list(rules) + + def __call__(self, *args: t.Any, **kwargs: t.Any) -> "RuleTemplateFactory": + return RuleTemplateFactory(self.rules, dict(*args, **kwargs)) + + +class RuleTemplateFactory(RuleFactory): + """A factory that fills in template variables into rules. Used by + `RuleTemplate` internally. + + :internal: + """ + + def __init__( + self, rules: t.Iterable[RuleFactory], context: t.Dict[str, t.Any] + ) -> None: + self.rules = rules + self.context = context + + def get_rules(self, map: "Map") -> t.Iterator["Rule"]: + for rulefactory in self.rules: + for rule in rulefactory.get_rules(map): + new_defaults = subdomain = None + if rule.defaults: + new_defaults = {} + for key, value in rule.defaults.items(): + if isinstance(value, str): + value = Template(value).substitute(self.context) + new_defaults[key] = value + if rule.subdomain is not None: + subdomain = Template(rule.subdomain).substitute(self.context) + new_endpoint = rule.endpoint + if isinstance(new_endpoint, str): + new_endpoint = Template(new_endpoint).substitute(self.context) + yield Rule( + Template(rule.rule).substitute(self.context), + new_defaults, + subdomain, + rule.methods, + rule.build_only, + new_endpoint, + rule.strict_slashes, + ) + + +def _prefix_names(src: str) -> ast.stmt: + """ast parse and prefix names with `.` to avoid collision with user vars""" + tree = ast.parse(src).body[0] + if isinstance(tree, ast.Expr): + tree = tree.value # type: ignore + for node in ast.walk(tree): + if isinstance(node, ast.Name): + node.id = f".{node.id}" + return tree + + +_CALL_CONVERTER_CODE_FMT = "self._converters[{elem!r}].to_url()" +_IF_KWARGS_URL_ENCODE_CODE = """\ +if kwargs: + q = '?' + params = self._encode_query_vars(kwargs) +else: + q = params = '' +""" +_IF_KWARGS_URL_ENCODE_AST = _prefix_names(_IF_KWARGS_URL_ENCODE_CODE) +_URL_ENCODE_AST_NAMES = (_prefix_names("q"), _prefix_names("params")) + + +class Rule(RuleFactory): + """A Rule represents one URL pattern. There are some options for `Rule` + that change the way it behaves and are passed to the `Rule` constructor. + Note that besides the rule-string all arguments *must* be keyword arguments + in order to not break the application on Werkzeug upgrades. + + `string` + Rule strings basically are just normal URL paths with placeholders in + the format ```` where the converter and the + arguments are optional. If no converter is defined the `default` + converter is used which means `string` in the normal configuration. + + URL rules that end with a slash are branch URLs, others are leaves. + If you have `strict_slashes` enabled (which is the default), all + branch URLs that are matched without a trailing slash will trigger a + redirect to the same URL with the missing slash appended. + + The converters are defined on the `Map`. + + `endpoint` + The endpoint for this rule. This can be anything. A reference to a + function, a string, a number etc. The preferred way is using a string + because the endpoint is used for URL generation. + + `defaults` + An optional dict with defaults for other rules with the same endpoint. + This is a bit tricky but useful if you want to have unique URLs:: + + url_map = Map([ + Rule('/all/', defaults={'page': 1}, endpoint='all_entries'), + Rule('/all/page/', endpoint='all_entries') + ]) + + If a user now visits ``http://example.com/all/page/1`` he will be + redirected to ``http://example.com/all/``. If `redirect_defaults` is + disabled on the `Map` instance this will only affect the URL + generation. + + `subdomain` + The subdomain rule string for this rule. If not specified the rule + only matches for the `default_subdomain` of the map. If the map is + not bound to a subdomain this feature is disabled. + + Can be useful if you want to have user profiles on different subdomains + and all subdomains are forwarded to your application:: + + url_map = Map([ + Rule('/', subdomain='', endpoint='user/homepage'), + Rule('/stats', subdomain='', endpoint='user/stats') + ]) + + `methods` + A sequence of http methods this rule applies to. If not specified, all + methods are allowed. For example this can be useful if you want different + endpoints for `POST` and `GET`. If methods are defined and the path + matches but the method matched against is not in this list or in the + list of another rule for that path the error raised is of the type + `MethodNotAllowed` rather than `NotFound`. If `GET` is present in the + list of methods and `HEAD` is not, `HEAD` is added automatically. + + `strict_slashes` + Override the `Map` setting for `strict_slashes` only for this rule. If + not specified the `Map` setting is used. + + `merge_slashes` + Override :attr:`Map.merge_slashes` for this rule. + + `build_only` + Set this to True and the rule will never match but will create a URL + that can be build. This is useful if you have resources on a subdomain + or folder that are not handled by the WSGI application (like static data) + + `redirect_to` + If given this must be either a string or callable. In case of a + callable it's called with the url adapter that triggered the match and + the values of the URL as keyword arguments and has to return the target + for the redirect, otherwise it has to be a string with placeholders in + rule syntax:: + + def foo_with_slug(adapter, id): + # ask the database for the slug for the old id. this of + # course has nothing to do with werkzeug. + return f'foo/{Foo.get_slug_for_id(id)}' + + url_map = Map([ + Rule('/foo/', endpoint='foo'), + Rule('/some/old/url/', redirect_to='foo/'), + Rule('/other/old/url/', redirect_to=foo_with_slug) + ]) + + When the rule is matched the routing system will raise a + `RequestRedirect` exception with the target for the redirect. + + Keep in mind that the URL will be joined against the URL root of the + script so don't use a leading slash on the target URL unless you + really mean root of that domain. + + `alias` + If enabled this rule serves as an alias for another rule with the same + endpoint and arguments. + + `host` + If provided and the URL map has host matching enabled this can be + used to provide a match rule for the whole host. This also means + that the subdomain feature is disabled. + + `websocket` + If ``True``, this rule is only matches for WebSocket (``ws://``, + ``wss://``) requests. By default, rules will only match for HTTP + requests. + + .. versionadded:: 1.0 + Added ``websocket``. + + .. versionadded:: 1.0 + Added ``merge_slashes``. + + .. versionadded:: 0.7 + Added ``alias`` and ``host``. + + .. versionchanged:: 0.6.1 + ``HEAD`` is added to ``methods`` if ``GET`` is present. + """ + + def __init__( + self, + string: str, + defaults: t.Optional[t.Mapping[str, t.Any]] = None, + subdomain: t.Optional[str] = None, + methods: t.Optional[t.Iterable[str]] = None, + build_only: bool = False, + endpoint: t.Optional[str] = None, + strict_slashes: t.Optional[bool] = None, + merge_slashes: t.Optional[bool] = None, + redirect_to: t.Optional[t.Union[str, t.Callable[..., str]]] = None, + alias: bool = False, + host: t.Optional[str] = None, + websocket: bool = False, + ) -> None: + if not string.startswith("/"): + raise ValueError("urls must start with a leading slash") + self.rule = string + self.is_leaf = not string.endswith("/") + + self.map: "Map" = None # type: ignore + self.strict_slashes = strict_slashes + self.merge_slashes = merge_slashes + self.subdomain = subdomain + self.host = host + self.defaults = defaults + self.build_only = build_only + self.alias = alias + self.websocket = websocket + + if methods is not None: + if isinstance(methods, str): + raise TypeError("'methods' should be a list of strings.") + + methods = {x.upper() for x in methods} + + if "HEAD" not in methods and "GET" in methods: + methods.add("HEAD") + + if websocket and methods - {"GET", "HEAD", "OPTIONS"}: + raise ValueError( + "WebSocket rules can only use 'GET', 'HEAD', and 'OPTIONS' methods." + ) + + self.methods = methods + self.endpoint: str = endpoint # type: ignore + self.redirect_to = redirect_to + + if defaults: + self.arguments = set(map(str, defaults)) + else: + self.arguments = set() + + self._trace: t.List[t.Tuple[bool, str]] = [] + + def empty(self) -> "Rule": + """ + Return an unbound copy of this rule. + + This can be useful if want to reuse an already bound URL for another + map. See ``get_empty_kwargs`` to override what keyword arguments are + provided to the new copy. + """ + return type(self)(self.rule, **self.get_empty_kwargs()) + + def get_empty_kwargs(self) -> t.Mapping[str, t.Any]: + """ + Provides kwargs for instantiating empty copy with empty() + + Use this method to provide custom keyword arguments to the subclass of + ``Rule`` when calling ``some_rule.empty()``. Helpful when the subclass + has custom keyword arguments that are needed at instantiation. + + Must return a ``dict`` that will be provided as kwargs to the new + instance of ``Rule``, following the initial ``self.rule`` value which + is always provided as the first, required positional argument. + """ + defaults = None + if self.defaults: + defaults = dict(self.defaults) + return dict( + defaults=defaults, + subdomain=self.subdomain, + methods=self.methods, + build_only=self.build_only, + endpoint=self.endpoint, + strict_slashes=self.strict_slashes, + redirect_to=self.redirect_to, + alias=self.alias, + host=self.host, + ) + + def get_rules(self, map: "Map") -> t.Iterator["Rule"]: + yield self + + def refresh(self) -> None: + """Rebinds and refreshes the URL. Call this if you modified the + rule in place. + + :internal: + """ + self.bind(self.map, rebind=True) + + def bind(self, map: "Map", rebind: bool = False) -> None: + """Bind the url to a map and create a regular expression based on + the information from the rule itself and the defaults from the map. + + :internal: + """ + if self.map is not None and not rebind: + raise RuntimeError(f"url rule {self!r} already bound to map {self.map!r}") + self.map = map + if self.strict_slashes is None: + self.strict_slashes = map.strict_slashes + if self.merge_slashes is None: + self.merge_slashes = map.merge_slashes + if self.subdomain is None: + self.subdomain = map.default_subdomain + self.compile() + + def get_converter( + self, + variable_name: str, + converter_name: str, + args: t.Tuple, + kwargs: t.Mapping[str, t.Any], + ) -> "BaseConverter": + """Looks up the converter for the given parameter. + + .. versionadded:: 0.9 + """ + if converter_name not in self.map.converters: + raise LookupError(f"the converter {converter_name!r} does not exist") + return self.map.converters[converter_name](self.map, *args, **kwargs) + + def _encode_query_vars(self, query_vars: t.Mapping[str, t.Any]) -> str: + return url_encode( + query_vars, + charset=self.map.charset, + sort=self.map.sort_parameters, + key=self.map.sort_key, + ) + + def compile(self) -> None: + """Compiles the regular expression and stores it.""" + assert self.map is not None, "rule not bound" + + if self.map.host_matching: + domain_rule = self.host or "" + else: + domain_rule = self.subdomain or "" + + self._trace = [] + self._converters: t.Dict[str, "BaseConverter"] = {} + self._static_weights: t.List[t.Tuple[int, int]] = [] + self._argument_weights: t.List[int] = [] + regex_parts = [] + + def _build_regex(rule: str) -> None: + index = 0 + for converter, arguments, variable in parse_rule(rule): + if converter is None: + for match in re.finditer(r"/+|[^/]+", variable): + part = match.group(0) + if part.startswith("/"): + if self.merge_slashes: + regex_parts.append(r"/+?") + self._trace.append((False, "/")) + else: + regex_parts.append(part) + self._trace.append((False, part)) + continue + self._trace.append((False, part)) + regex_parts.append(re.escape(part)) + if part: + self._static_weights.append((index, -len(part))) + else: + if arguments: + c_args, c_kwargs = parse_converter_args(arguments) + else: + c_args = () + c_kwargs = {} + convobj = self.get_converter(variable, converter, c_args, c_kwargs) + regex_parts.append(f"(?P<{variable}>{convobj.regex})") + self._converters[variable] = convobj + self._trace.append((True, variable)) + self._argument_weights.append(convobj.weight) + self.arguments.add(str(variable)) + index = index + 1 + + _build_regex(domain_rule) + regex_parts.append("\\|") + self._trace.append((False, "|")) + _build_regex(self.rule if self.is_leaf else self.rule.rstrip("/")) + if not self.is_leaf: + self._trace.append((False, "/")) + + self._build: t.Callable[..., t.Tuple[str, str]] + self._build = self._compile_builder(False).__get__(self, None) # type: ignore + self._build_unknown: t.Callable[..., t.Tuple[str, str]] + self._build_unknown = self._compile_builder(True).__get__( # type: ignore + self, None + ) + + if self.build_only: + return + + if not (self.is_leaf and self.strict_slashes): + reps = "*" if self.merge_slashes else "?" + tail = f"(?/{reps})" + else: + tail = "" + + regex = f"^{''.join(regex_parts)}{tail}$" + self._regex = re.compile(regex) + + def match( + self, path: str, method: t.Optional[str] = None + ) -> t.Optional[t.MutableMapping[str, t.Any]]: + """Check if the rule matches a given path. Path is a string in the + form ``"subdomain|/path"`` and is assembled by the map. If + the map is doing host matching the subdomain part will be the host + instead. + + If the rule matches a dict with the converted values is returned, + otherwise the return value is `None`. + + :internal: + """ + if not self.build_only: + require_redirect = False + + m = self._regex.search(path) + if m is not None: + groups = m.groupdict() + # we have a folder like part of the url without a trailing + # slash and strict slashes enabled. raise an exception that + # tells the map to redirect to the same url but with a + # trailing slash + if ( + self.strict_slashes + and not self.is_leaf + and not groups.pop("__suffix__") + and ( + method is None or self.methods is None or method in self.methods + ) + ): + path += "/" + require_redirect = True + # if we are not in strict slashes mode we have to remove + # a __suffix__ + elif not self.strict_slashes: + del groups["__suffix__"] + + result = {} + for name, value in groups.items(): + try: + value = self._converters[name].to_python(value) + except ValidationError: + return None + result[str(name)] = value + if self.defaults: + result.update(self.defaults) + + if self.merge_slashes: + new_path = "|".join(self.build(result, False)) # type: ignore + if path.endswith("/") and not new_path.endswith("/"): + new_path += "/" + if new_path.count("/") < path.count("/"): + # The URL will be encoded when MapAdapter.match + # handles the RequestPath raised below. Decode + # the URL here to avoid a double encoding. + path = url_unquote(new_path) + require_redirect = True + + if require_redirect: + path = path.split("|", 1)[1] + raise RequestPath(path) + + if self.alias and self.map.redirect_defaults: + raise RequestAliasRedirect(result) + + return result + + return None + + @staticmethod + def _get_func_code(code: CodeType, name: str) -> t.Callable[..., t.Tuple[str, str]]: + globs: t.Dict[str, t.Any] = {} + locs: t.Dict[str, t.Any] = {} + exec(code, globs, locs) + return locs[name] # type: ignore + + def _compile_builder( + self, append_unknown: bool = True + ) -> t.Callable[..., t.Tuple[str, str]]: + defaults = self.defaults or {} + dom_ops: t.List[t.Tuple[bool, str]] = [] + url_ops: t.List[t.Tuple[bool, str]] = [] + + opl = dom_ops + for is_dynamic, data in self._trace: + if data == "|" and opl is dom_ops: + opl = url_ops + continue + # this seems like a silly case to ever come up but: + # if a default is given for a value that appears in the rule, + # resolve it to a constant ahead of time + if is_dynamic and data in defaults: + data = self._converters[data].to_url(defaults[data]) + opl.append((False, data)) + elif not is_dynamic: + opl.append( + (False, url_quote(_to_bytes(data, self.map.charset), safe="/:|+")) + ) + else: + opl.append((True, data)) + + def _convert(elem: str) -> ast.stmt: + ret = _prefix_names(_CALL_CONVERTER_CODE_FMT.format(elem=elem)) + ret.args = [ast.Name(str(elem), ast.Load())] # type: ignore # str for py2 + return ret + + def _parts(ops: t.List[t.Tuple[bool, str]]) -> t.List[ast.AST]: + parts = [ + _convert(elem) if is_dynamic else ast.Str(s=elem) + for is_dynamic, elem in ops + ] + parts = parts or [ast.Str("")] + # constant fold + ret = [parts[0]] + for p in parts[1:]: + if isinstance(p, ast.Str) and isinstance(ret[-1], ast.Str): + ret[-1] = ast.Str(ret[-1].s + p.s) + else: + ret.append(p) + return ret + + dom_parts = _parts(dom_ops) + url_parts = _parts(url_ops) + if not append_unknown: + body = [] + else: + body = [_IF_KWARGS_URL_ENCODE_AST] + url_parts.extend(_URL_ENCODE_AST_NAMES) + + def _join(parts: t.List[ast.AST]) -> ast.AST: + if len(parts) == 1: # shortcut + return parts[0] + return ast.JoinedStr(parts) + + body.append( + ast.Return(ast.Tuple([_join(dom_parts), _join(url_parts)], ast.Load())) + ) + + pargs = [ + elem + for is_dynamic, elem in dom_ops + url_ops + if is_dynamic and elem not in defaults + ] + kargs = [str(k) for k in defaults] + + func_ast: ast.FunctionDef = _prefix_names("def _(): pass") # type: ignore + func_ast.name = f"" + func_ast.args.args.append(ast.arg(".self", None)) + for arg in pargs + kargs: + func_ast.args.args.append(ast.arg(arg, None)) + func_ast.args.kwarg = ast.arg(".kwargs", None) + for _ in kargs: + func_ast.args.defaults.append(ast.Str("")) + func_ast.body = body + + # use `ast.parse` instead of `ast.Module` for better portability + # Python 3.8 changes the signature of `ast.Module` + module = ast.parse("") + module.body = [func_ast] + + # mark everything as on line 1, offset 0 + # less error-prone than `ast.fix_missing_locations` + # bad line numbers cause an assert to fail in debug builds + for node in ast.walk(module): + if "lineno" in node._attributes: + node.lineno = 1 + if "col_offset" in node._attributes: + node.col_offset = 0 + + code = compile(module, "", "exec") + return self._get_func_code(code, func_ast.name) + + def build( + self, values: t.Mapping[str, t.Any], append_unknown: bool = True + ) -> t.Optional[t.Tuple[str, str]]: + """Assembles the relative url for that rule and the subdomain. + If building doesn't work for some reasons `None` is returned. + + :internal: + """ + try: + if append_unknown: + return self._build_unknown(**values) + else: + return self._build(**values) + except ValidationError: + return None + + def provides_defaults_for(self, rule: "Rule") -> bool: + """Check if this rule has defaults for a given rule. + + :internal: + """ + return bool( + not self.build_only + and self.defaults + and self.endpoint == rule.endpoint + and self != rule + and self.arguments == rule.arguments + ) + + def suitable_for( + self, values: t.Mapping[str, t.Any], method: t.Optional[str] = None + ) -> bool: + """Check if the dict of values has enough data for url generation. + + :internal: + """ + # if a method was given explicitly and that method is not supported + # by this rule, this rule is not suitable. + if ( + method is not None + and self.methods is not None + and method not in self.methods + ): + return False + + defaults = self.defaults or () + + # all arguments required must be either in the defaults dict or + # the value dictionary otherwise it's not suitable + for key in self.arguments: + if key not in defaults and key not in values: + return False + + # in case defaults are given we ensure that either the value was + # skipped or the value is the same as the default value. + if defaults: + for key, value in defaults.items(): + if key in values and value != values[key]: + return False + + return True + + def match_compare_key( + self, + ) -> t.Tuple[bool, int, t.Iterable[t.Tuple[int, int]], int, t.Iterable[int]]: + """The match compare key for sorting. + + Current implementation: + + 1. rules without any arguments come first for performance + reasons only as we expect them to match faster and some + common ones usually don't have any arguments (index pages etc.) + 2. rules with more static parts come first so the second argument + is the negative length of the number of the static weights. + 3. we order by static weights, which is a combination of index + and length + 4. The more complex rules come first so the next argument is the + negative length of the number of argument weights. + 5. lastly we order by the actual argument weights. + + :internal: + """ + return ( + bool(self.arguments), + -len(self._static_weights), + self._static_weights, + -len(self._argument_weights), + self._argument_weights, + ) + + def build_compare_key(self) -> t.Tuple[int, int, int]: + """The build compare key for sorting. + + :internal: + """ + return (1 if self.alias else 0, -len(self.arguments), -len(self.defaults or ())) + + def __eq__(self, other: object) -> bool: + return isinstance(other, type(self)) and self._trace == other._trace + + __hash__ = None # type: ignore + + def __str__(self) -> str: + return self.rule + + def __repr__(self) -> str: + if self.map is None: + return f"<{type(self).__name__} (unbound)>" + parts = [] + for is_dynamic, data in self._trace: + if is_dynamic: + parts.append(f"<{data}>") + else: + parts.append(data) + parts = "".join(parts).lstrip("|") + methods = f" ({', '.join(self.methods)})" if self.methods is not None else "" + return f"<{type(self).__name__} {parts!r}{methods} -> {self.endpoint}>" + + +class BaseConverter: + """Base class for all converters.""" + + regex = "[^/]+" + weight = 100 + + def __init__(self, map: "Map", *args: t.Any, **kwargs: t.Any) -> None: + self.map = map + + def to_python(self, value: str) -> t.Any: + return value + + def to_url(self, value: t.Any) -> str: + if isinstance(value, (bytes, bytearray)): + return _fast_url_quote(value) + return _fast_url_quote(str(value).encode(self.map.charset)) + + +class UnicodeConverter(BaseConverter): + """This converter is the default converter and accepts any string but + only one path segment. Thus the string can not include a slash. + + This is the default validator. + + Example:: + + Rule('/pages/'), + Rule('/') + + :param map: the :class:`Map`. + :param minlength: the minimum length of the string. Must be greater + or equal 1. + :param maxlength: the maximum length of the string. + :param length: the exact length of the string. + """ + + def __init__( + self, + map: "Map", + minlength: int = 1, + maxlength: t.Optional[int] = None, + length: t.Optional[int] = None, + ) -> None: + super().__init__(map) + if length is not None: + length_regex = f"{{{int(length)}}}" + else: + if maxlength is None: + maxlength_value = "" + else: + maxlength_value = str(int(maxlength)) + length_regex = f"{{{int(minlength)},{maxlength_value}}}" + self.regex = f"[^/]{length_regex}" + + +class AnyConverter(BaseConverter): + """Matches one of the items provided. Items can either be Python + identifiers or strings:: + + Rule('/') + + :param map: the :class:`Map`. + :param items: this function accepts the possible items as positional + arguments. + """ + + def __init__(self, map: "Map", *items: str) -> None: + super().__init__(map) + self.regex = f"(?:{'|'.join([re.escape(x) for x in items])})" + + +class PathConverter(BaseConverter): + """Like the default :class:`UnicodeConverter`, but it also matches + slashes. This is useful for wikis and similar applications:: + + Rule('/') + Rule('//edit') + + :param map: the :class:`Map`. + """ + + regex = "[^/].*?" + weight = 200 + + +class NumberConverter(BaseConverter): + """Baseclass for `IntegerConverter` and `FloatConverter`. + + :internal: + """ + + weight = 50 + num_convert: t.Callable = int + + def __init__( + self, + map: "Map", + fixed_digits: int = 0, + min: t.Optional[int] = None, + max: t.Optional[int] = None, + signed: bool = False, + ) -> None: + if signed: + self.regex = self.signed_regex + super().__init__(map) + self.fixed_digits = fixed_digits + self.min = min + self.max = max + self.signed = signed + + def to_python(self, value: str) -> t.Any: + if self.fixed_digits and len(value) != self.fixed_digits: + raise ValidationError() + value = self.num_convert(value) + if (self.min is not None and value < self.min) or ( + self.max is not None and value > self.max + ): + raise ValidationError() + return value + + def to_url(self, value: t.Any) -> str: + value = str(self.num_convert(value)) + if self.fixed_digits: + value = value.zfill(self.fixed_digits) + return value + + @property + def signed_regex(self) -> str: + return f"-?{self.regex}" + + +class IntegerConverter(NumberConverter): + """This converter only accepts integer values:: + + Rule("/page/") + + By default it only accepts unsigned, positive values. The ``signed`` + parameter will enable signed, negative values. :: + + Rule("/page/") + + :param map: The :class:`Map`. + :param fixed_digits: The number of fixed digits in the URL. If you + set this to ``4`` for example, the rule will only match if the + URL looks like ``/0001/``. The default is variable length. + :param min: The minimal value. + :param max: The maximal value. + :param signed: Allow signed (negative) values. + + .. versionadded:: 0.15 + The ``signed`` parameter. + """ + + regex = r"\d+" + + +class FloatConverter(NumberConverter): + """This converter only accepts floating point values:: + + Rule("/probability/") + + By default it only accepts unsigned, positive values. The ``signed`` + parameter will enable signed, negative values. :: + + Rule("/offset/") + + :param map: The :class:`Map`. + :param min: The minimal value. + :param max: The maximal value. + :param signed: Allow signed (negative) values. + + .. versionadded:: 0.15 + The ``signed`` parameter. + """ + + regex = r"\d+\.\d+" + num_convert = float + + def __init__( + self, + map: "Map", + min: t.Optional[float] = None, + max: t.Optional[float] = None, + signed: bool = False, + ) -> None: + super().__init__(map, min=min, max=max, signed=signed) # type: ignore + + +class UUIDConverter(BaseConverter): + """This converter only accepts UUID strings:: + + Rule('/object/') + + .. versionadded:: 0.10 + + :param map: the :class:`Map`. + """ + + regex = ( + r"[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-" + r"[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}" + ) + + def to_python(self, value: str) -> uuid.UUID: + return uuid.UUID(value) + + def to_url(self, value: uuid.UUID) -> str: + return str(value) + + +#: the default converter mapping for the map. +DEFAULT_CONVERTERS: t.Mapping[str, t.Type[BaseConverter]] = { + "default": UnicodeConverter, + "string": UnicodeConverter, + "any": AnyConverter, + "path": PathConverter, + "int": IntegerConverter, + "float": FloatConverter, + "uuid": UUIDConverter, +} + + +class Map: + """The map class stores all the URL rules and some configuration + parameters. Some of the configuration values are only stored on the + `Map` instance since those affect all rules, others are just defaults + and can be overridden for each rule. Note that you have to specify all + arguments besides the `rules` as keyword arguments! + + :param rules: sequence of url rules for this map. + :param default_subdomain: The default subdomain for rules without a + subdomain defined. + :param charset: charset of the url. defaults to ``"utf-8"`` + :param strict_slashes: If a rule ends with a slash but the matched + URL does not, redirect to the URL with a trailing slash. + :param merge_slashes: Merge consecutive slashes when matching or + building URLs. Matches will redirect to the normalized URL. + Slashes in variable parts are not merged. + :param redirect_defaults: This will redirect to the default rule if it + wasn't visited that way. This helps creating + unique URLs. + :param converters: A dict of converters that adds additional converters + to the list of converters. If you redefine one + converter this will override the original one. + :param sort_parameters: If set to `True` the url parameters are sorted. + See `url_encode` for more details. + :param sort_key: The sort key function for `url_encode`. + :param encoding_errors: the error method to use for decoding + :param host_matching: if set to `True` it enables the host matching + feature and disables the subdomain one. If + enabled the `host` parameter to rules is used + instead of the `subdomain` one. + + .. versionchanged:: 1.0 + If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules + will match. + + .. versionchanged:: 1.0 + Added ``merge_slashes``. + + .. versionchanged:: 0.7 + Added ``encoding_errors`` and ``host_matching``. + + .. versionchanged:: 0.5 + Added ``sort_parameters`` and ``sort_key``. + """ + + #: A dict of default converters to be used. + default_converters = ImmutableDict(DEFAULT_CONVERTERS) + + #: The type of lock to use when updating. + #: + #: .. versionadded:: 1.0 + lock_class = Lock + + def __init__( + self, + rules: t.Optional[t.Iterable[RuleFactory]] = None, + default_subdomain: str = "", + charset: str = "utf-8", + strict_slashes: bool = True, + merge_slashes: bool = True, + redirect_defaults: bool = True, + converters: t.Optional[t.Mapping[str, t.Type[BaseConverter]]] = None, + sort_parameters: bool = False, + sort_key: t.Optional[t.Callable[[t.Any], t.Any]] = None, + encoding_errors: str = "replace", + host_matching: bool = False, + ) -> None: + self._rules: t.List[Rule] = [] + self._rules_by_endpoint: t.Dict[str, t.List[Rule]] = {} + self._remap = True + self._remap_lock = self.lock_class() + + self.default_subdomain = default_subdomain + self.charset = charset + self.encoding_errors = encoding_errors + self.strict_slashes = strict_slashes + self.merge_slashes = merge_slashes + self.redirect_defaults = redirect_defaults + self.host_matching = host_matching + + self.converters = self.default_converters.copy() + if converters: + self.converters.update(converters) + + self.sort_parameters = sort_parameters + self.sort_key = sort_key + + for rulefactory in rules or (): + self.add(rulefactory) + + def is_endpoint_expecting(self, endpoint: str, *arguments: str) -> bool: + """Iterate over all rules and check if the endpoint expects + the arguments provided. This is for example useful if you have + some URLs that expect a language code and others that do not and + you want to wrap the builder a bit so that the current language + code is automatically added if not provided but endpoints expect + it. + + :param endpoint: the endpoint to check. + :param arguments: this function accepts one or more arguments + as positional arguments. Each one of them is + checked. + """ + self.update() + arguments = set(arguments) + for rule in self._rules_by_endpoint[endpoint]: + if arguments.issubset(rule.arguments): + return True + return False + + def iter_rules(self, endpoint: t.Optional[str] = None) -> t.Iterator[Rule]: + """Iterate over all rules or the rules of an endpoint. + + :param endpoint: if provided only the rules for that endpoint + are returned. + :return: an iterator + """ + self.update() + if endpoint is not None: + return iter(self._rules_by_endpoint[endpoint]) + return iter(self._rules) + + def add(self, rulefactory: RuleFactory) -> None: + """Add a new rule or factory to the map and bind it. Requires that the + rule is not bound to another map. + + :param rulefactory: a :class:`Rule` or :class:`RuleFactory` + """ + for rule in rulefactory.get_rules(self): + rule.bind(self) + self._rules.append(rule) + self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule) + self._remap = True + + def bind( + self, + server_name: str, + script_name: t.Optional[str] = None, + subdomain: t.Optional[str] = None, + url_scheme: str = "http", + default_method: str = "GET", + path_info: t.Optional[str] = None, + query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None, + ) -> "MapAdapter": + """Return a new :class:`MapAdapter` with the details specified to the + call. Note that `script_name` will default to ``'/'`` if not further + specified or `None`. The `server_name` at least is a requirement + because the HTTP RFC requires absolute URLs for redirects and so all + redirect exceptions raised by Werkzeug will contain the full canonical + URL. + + If no path_info is passed to :meth:`match` it will use the default path + info passed to bind. While this doesn't really make sense for + manual bind calls, it's useful if you bind a map to a WSGI + environment which already contains the path info. + + `subdomain` will default to the `default_subdomain` for this map if + no defined. If there is no `default_subdomain` you cannot use the + subdomain feature. + + .. versionchanged:: 1.0 + If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules + will match. + + .. versionchanged:: 0.15 + ``path_info`` defaults to ``'/'`` if ``None``. + + .. versionchanged:: 0.8 + ``query_args`` can be a string. + + .. versionchanged:: 0.7 + Added ``query_args``. + """ + server_name = server_name.lower() + if self.host_matching: + if subdomain is not None: + raise RuntimeError("host matching enabled and a subdomain was provided") + elif subdomain is None: + subdomain = self.default_subdomain + if script_name is None: + script_name = "/" + if path_info is None: + path_info = "/" + + try: + server_name = _encode_idna(server_name) # type: ignore + except UnicodeError as e: + raise BadHost() from e + + return MapAdapter( + self, + server_name, + script_name, + subdomain, + url_scheme, + path_info, + default_method, + query_args, + ) + + def bind_to_environ( + self, + environ: "WSGIEnvironment", + server_name: t.Optional[str] = None, + subdomain: t.Optional[str] = None, + ) -> "MapAdapter": + """Like :meth:`bind` but you can pass it an WSGI environment and it + will fetch the information from that dictionary. Note that because of + limitations in the protocol there is no way to get the current + subdomain and real `server_name` from the environment. If you don't + provide it, Werkzeug will use `SERVER_NAME` and `SERVER_PORT` (or + `HTTP_HOST` if provided) as used `server_name` with disabled subdomain + feature. + + If `subdomain` is `None` but an environment and a server name is + provided it will calculate the current subdomain automatically. + Example: `server_name` is ``'example.com'`` and the `SERVER_NAME` + in the wsgi `environ` is ``'staging.dev.example.com'`` the calculated + subdomain will be ``'staging.dev'``. + + If the object passed as environ has an environ attribute, the value of + this attribute is used instead. This allows you to pass request + objects. Additionally `PATH_INFO` added as a default of the + :class:`MapAdapter` so that you don't have to pass the path info to + the match method. + + .. versionchanged:: 1.0.0 + If the passed server name specifies port 443, it will match + if the incoming scheme is ``https`` without a port. + + .. versionchanged:: 1.0.0 + A warning is shown when the passed server name does not + match the incoming WSGI server name. + + .. versionchanged:: 0.8 + This will no longer raise a ValueError when an unexpected server + name was passed. + + .. versionchanged:: 0.5 + previously this method accepted a bogus `calculate_subdomain` + parameter that did not have any effect. It was removed because + of that. + + :param environ: a WSGI environment. + :param server_name: an optional server name hint (see above). + :param subdomain: optionally the current subdomain (see above). + """ + environ = _get_environ(environ) + wsgi_server_name = get_host(environ).lower() + scheme = environ["wsgi.url_scheme"] + upgrade = any( + v.strip() == "upgrade" + for v in environ.get("HTTP_CONNECTION", "").lower().split(",") + ) + + if upgrade and environ.get("HTTP_UPGRADE", "").lower() == "websocket": + scheme = "wss" if scheme == "https" else "ws" + + if server_name is None: + server_name = wsgi_server_name + else: + server_name = server_name.lower() + + # strip standard port to match get_host() + if scheme in {"http", "ws"} and server_name.endswith(":80"): + server_name = server_name[:-3] + elif scheme in {"https", "wss"} and server_name.endswith(":443"): + server_name = server_name[:-4] + + if subdomain is None and not self.host_matching: + cur_server_name = wsgi_server_name.split(".") + real_server_name = server_name.split(".") + offset = -len(real_server_name) + + if cur_server_name[offset:] != real_server_name: + # This can happen even with valid configs if the server was + # accessed directly by IP address under some situations. + # Instead of raising an exception like in Werkzeug 0.7 or + # earlier we go by an invalid subdomain which will result + # in a 404 error on matching. + warnings.warn( + f"Current server name {wsgi_server_name!r} doesn't match configured" + f" server name {server_name!r}", + stacklevel=2, + ) + subdomain = "" + else: + subdomain = ".".join(filter(None, cur_server_name[:offset])) + + def _get_wsgi_string(name: str) -> t.Optional[str]: + val = environ.get(name) + if val is not None: + return _wsgi_decoding_dance(val, self.charset) + return None + + script_name = _get_wsgi_string("SCRIPT_NAME") + path_info = _get_wsgi_string("PATH_INFO") + query_args = _get_wsgi_string("QUERY_STRING") + return Map.bind( + self, + server_name, + script_name, + subdomain, + scheme, + environ["REQUEST_METHOD"], + path_info, + query_args=query_args, + ) + + def update(self) -> None: + """Called before matching and building to keep the compiled rules + in the correct order after things changed. + """ + if not self._remap: + return + + with self._remap_lock: + if not self._remap: + return + + self._rules.sort(key=lambda x: x.match_compare_key()) + for rules in self._rules_by_endpoint.values(): + rules.sort(key=lambda x: x.build_compare_key()) + self._remap = False + + def __repr__(self) -> str: + rules = self.iter_rules() + return f"{type(self).__name__}({pformat(list(rules))})" + + +class MapAdapter: + + """Returned by :meth:`Map.bind` or :meth:`Map.bind_to_environ` and does + the URL matching and building based on runtime information. + """ + + def __init__( + self, + map: Map, + server_name: str, + script_name: str, + subdomain: t.Optional[str], + url_scheme: str, + path_info: str, + default_method: str, + query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None, + ): + self.map = map + self.server_name = _to_str(server_name) + script_name = _to_str(script_name) + if not script_name.endswith("/"): + script_name += "/" + self.script_name = script_name + self.subdomain = _to_str(subdomain) + self.url_scheme = _to_str(url_scheme) + self.path_info = _to_str(path_info) + self.default_method = _to_str(default_method) + self.query_args = query_args + self.websocket = self.url_scheme in {"ws", "wss"} + + def dispatch( + self, + view_func: t.Callable[[str, t.Mapping[str, t.Any]], "WSGIApplication"], + path_info: t.Optional[str] = None, + method: t.Optional[str] = None, + catch_http_exceptions: bool = False, + ) -> "WSGIApplication": + """Does the complete dispatching process. `view_func` is called with + the endpoint and a dict with the values for the view. It should + look up the view function, call it, and return a response object + or WSGI application. http exceptions are not caught by default + so that applications can display nicer error messages by just + catching them by hand. If you want to stick with the default + error messages you can pass it ``catch_http_exceptions=True`` and + it will catch the http exceptions. + + Here a small example for the dispatch usage:: + + from werkzeug.wrappers import Request, Response + from werkzeug.wsgi import responder + from werkzeug.routing import Map, Rule + + def on_index(request): + return Response('Hello from the index') + + url_map = Map([Rule('/', endpoint='index')]) + views = {'index': on_index} + + @responder + def application(environ, start_response): + request = Request(environ) + urls = url_map.bind_to_environ(environ) + return urls.dispatch(lambda e, v: views[e](request, **v), + catch_http_exceptions=True) + + Keep in mind that this method might return exception objects, too, so + use :class:`Response.force_type` to get a response object. + + :param view_func: a function that is called with the endpoint as + first argument and the value dict as second. Has + to dispatch to the actual view function with this + information. (see above) + :param path_info: the path info to use for matching. Overrides the + path info specified on binding. + :param method: the HTTP method used for matching. Overrides the + method specified on binding. + :param catch_http_exceptions: set to `True` to catch any of the + werkzeug :class:`HTTPException`\\s. + """ + try: + try: + endpoint, args = self.match(path_info, method) + except RequestRedirect as e: + return e + return view_func(endpoint, args) + except HTTPException as e: + if catch_http_exceptions: + return e + raise + + @typing.overload + def match( # type: ignore + self, + path_info: t.Optional[str] = None, + method: t.Optional[str] = None, + return_rule: "te.Literal[False]" = False, + query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None, + websocket: t.Optional[bool] = None, + ) -> t.Tuple[str, t.Mapping[str, t.Any]]: + ... + + @typing.overload + def match( + self, + path_info: t.Optional[str] = None, + method: t.Optional[str] = None, + return_rule: "te.Literal[True]" = True, + query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None, + websocket: t.Optional[bool] = None, + ) -> t.Tuple[Rule, t.Mapping[str, t.Any]]: + ... + + def match( + self, + path_info: t.Optional[str] = None, + method: t.Optional[str] = None, + return_rule: bool = False, + query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None, + websocket: t.Optional[bool] = None, + ) -> t.Tuple[t.Union[str, Rule], t.Mapping[str, t.Any]]: + """The usage is simple: you just pass the match method the current + path info as well as the method (which defaults to `GET`). The + following things can then happen: + + - you receive a `NotFound` exception that indicates that no URL is + matching. A `NotFound` exception is also a WSGI application you + can call to get a default page not found page (happens to be the + same object as `werkzeug.exceptions.NotFound`) + + - you receive a `MethodNotAllowed` exception that indicates that there + is a match for this URL but not for the current request method. + This is useful for RESTful applications. + + - you receive a `RequestRedirect` exception with a `new_url` + attribute. This exception is used to notify you about a request + Werkzeug requests from your WSGI application. This is for example the + case if you request ``/foo`` although the correct URL is ``/foo/`` + You can use the `RequestRedirect` instance as response-like object + similar to all other subclasses of `HTTPException`. + + - you receive a ``WebsocketMismatch`` exception if the only + match is a WebSocket rule but the bind is an HTTP request, or + if the match is an HTTP rule but the bind is a WebSocket + request. + + - you get a tuple in the form ``(endpoint, arguments)`` if there is + a match (unless `return_rule` is True, in which case you get a tuple + in the form ``(rule, arguments)``) + + If the path info is not passed to the match method the default path + info of the map is used (defaults to the root URL if not defined + explicitly). + + All of the exceptions raised are subclasses of `HTTPException` so they + can be used as WSGI responses. They will all render generic error or + redirect pages. + + Here is a small example for matching: + + >>> m = Map([ + ... Rule('/', endpoint='index'), + ... Rule('/downloads/', endpoint='downloads/index'), + ... Rule('/downloads/', endpoint='downloads/show') + ... ]) + >>> urls = m.bind("example.com", "/") + >>> urls.match("/", "GET") + ('index', {}) + >>> urls.match("/downloads/42") + ('downloads/show', {'id': 42}) + + And here is what happens on redirect and missing URLs: + + >>> urls.match("/downloads") + Traceback (most recent call last): + ... + RequestRedirect: http://example.com/downloads/ + >>> urls.match("/missing") + Traceback (most recent call last): + ... + NotFound: 404 Not Found + + :param path_info: the path info to use for matching. Overrides the + path info specified on binding. + :param method: the HTTP method used for matching. Overrides the + method specified on binding. + :param return_rule: return the rule that matched instead of just the + endpoint (defaults to `False`). + :param query_args: optional query arguments that are used for + automatic redirects as string or dictionary. It's + currently not possible to use the query arguments + for URL matching. + :param websocket: Match WebSocket instead of HTTP requests. A + websocket request has a ``ws`` or ``wss`` + :attr:`url_scheme`. This overrides that detection. + + .. versionadded:: 1.0 + Added ``websocket``. + + .. versionchanged:: 0.8 + ``query_args`` can be a string. + + .. versionadded:: 0.7 + Added ``query_args``. + + .. versionadded:: 0.6 + Added ``return_rule``. + """ + self.map.update() + if path_info is None: + path_info = self.path_info + else: + path_info = _to_str(path_info, self.map.charset) + if query_args is None: + query_args = self.query_args or {} + method = (method or self.default_method).upper() + + if websocket is None: + websocket = self.websocket + + require_redirect = False + + domain_part = self.server_name if self.map.host_matching else self.subdomain + path_part = f"/{path_info.lstrip('/')}" if path_info else "" + path = f"{domain_part}|{path_part}" + + have_match_for = set() + websocket_mismatch = False + + for rule in self.map._rules: + try: + rv = rule.match(path, method) + except RequestPath as e: + raise RequestRedirect( + self.make_redirect_url( + url_quote(e.path_info, self.map.charset, safe="/:|+"), + query_args, + ) + ) from None + except RequestAliasRedirect as e: + raise RequestRedirect( + self.make_alias_redirect_url( + path, rule.endpoint, e.matched_values, method, query_args + ) + ) from None + if rv is None: + continue + if rule.methods is not None and method not in rule.methods: + have_match_for.update(rule.methods) + continue + + if rule.websocket != websocket: + websocket_mismatch = True + continue + + if self.map.redirect_defaults: + redirect_url = self.get_default_redirect(rule, method, rv, query_args) + if redirect_url is not None: + raise RequestRedirect(redirect_url) + + if rule.redirect_to is not None: + if isinstance(rule.redirect_to, str): + + def _handle_match(match: t.Match[str]) -> str: + value = rv[match.group(1)] # type: ignore + return rule._converters[match.group(1)].to_url(value) + + redirect_url = _simple_rule_re.sub(_handle_match, rule.redirect_to) + else: + redirect_url = rule.redirect_to(self, **rv) + + if self.subdomain: + netloc = f"{self.subdomain}.{self.server_name}" + else: + netloc = self.server_name + + raise RequestRedirect( + url_join( + f"{self.url_scheme or 'http'}://{netloc}{self.script_name}", + redirect_url, + ) + ) + + if require_redirect: + raise RequestRedirect( + self.make_redirect_url( + url_quote(path_info, self.map.charset, safe="/:|+"), query_args + ) + ) + + if return_rule: + return rule, rv + else: + return rule.endpoint, rv + + if have_match_for: + raise MethodNotAllowed(valid_methods=list(have_match_for)) + + if websocket_mismatch: + raise WebsocketMismatch() + + raise NotFound() + + def test( + self, path_info: t.Optional[str] = None, method: t.Optional[str] = None + ) -> bool: + """Test if a rule would match. Works like `match` but returns `True` + if the URL matches, or `False` if it does not exist. + + :param path_info: the path info to use for matching. Overrides the + path info specified on binding. + :param method: the HTTP method used for matching. Overrides the + method specified on binding. + """ + try: + self.match(path_info, method) + except RequestRedirect: + pass + except HTTPException: + return False + return True + + def allowed_methods(self, path_info: t.Optional[str] = None) -> t.Iterable[str]: + """Returns the valid methods that match for a given path. + + .. versionadded:: 0.7 + """ + try: + self.match(path_info, method="--") + except MethodNotAllowed as e: + return e.valid_methods # type: ignore + except HTTPException: + pass + return [] + + def get_host(self, domain_part: t.Optional[str]) -> str: + """Figures out the full host name for the given domain part. The + domain part is a subdomain in case host matching is disabled or + a full host name. + """ + if self.map.host_matching: + if domain_part is None: + return self.server_name + return _to_str(domain_part, "ascii") + subdomain = domain_part + if subdomain is None: + subdomain = self.subdomain + else: + subdomain = _to_str(subdomain, "ascii") + + if subdomain: + return f"{subdomain}.{self.server_name}" + else: + return self.server_name + + def get_default_redirect( + self, + rule: Rule, + method: str, + values: t.MutableMapping[str, t.Any], + query_args: t.Union[t.Mapping[str, t.Any], str], + ) -> t.Optional[str]: + """A helper that returns the URL to redirect to if it finds one. + This is used for default redirecting only. + + :internal: + """ + assert self.map.redirect_defaults + for r in self.map._rules_by_endpoint[rule.endpoint]: + # every rule that comes after this one, including ourself + # has a lower priority for the defaults. We order the ones + # with the highest priority up for building. + if r is rule: + break + if r.provides_defaults_for(rule) and r.suitable_for(values, method): + values.update(r.defaults) # type: ignore + domain_part, path = r.build(values) # type: ignore + return self.make_redirect_url(path, query_args, domain_part=domain_part) + return None + + def encode_query_args(self, query_args: t.Union[t.Mapping[str, t.Any], str]) -> str: + if not isinstance(query_args, str): + return url_encode(query_args, self.map.charset) + return query_args + + def make_redirect_url( + self, + path_info: str, + query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None, + domain_part: t.Optional[str] = None, + ) -> str: + """Creates a redirect URL. + + :internal: + """ + if query_args: + suffix = f"?{self.encode_query_args(query_args)}" + else: + suffix = "" + + scheme = self.url_scheme or "http" + host = self.get_host(domain_part) + path = posixpath.join(self.script_name.strip("/"), path_info.lstrip("/")) + return f"{scheme}://{host}/{path}{suffix}" + + def make_alias_redirect_url( + self, + path: str, + endpoint: str, + values: t.Mapping[str, t.Any], + method: str, + query_args: t.Union[t.Mapping[str, t.Any], str], + ) -> str: + """Internally called to make an alias redirect URL.""" + url = self.build( + endpoint, values, method, append_unknown=False, force_external=True + ) + if query_args: + url += f"?{self.encode_query_args(query_args)}" + assert url != path, "detected invalid alias setting. No canonical URL found" + return url + + def _partial_build( + self, + endpoint: str, + values: t.Mapping[str, t.Any], + method: t.Optional[str], + append_unknown: bool, + ) -> t.Optional[t.Tuple[str, str, bool]]: + """Helper for :meth:`build`. Returns subdomain and path for the + rule that accepts this endpoint, values and method. + + :internal: + """ + # in case the method is none, try with the default method first + if method is None: + rv = self._partial_build( + endpoint, values, self.default_method, append_unknown + ) + if rv is not None: + return rv + + # Default method did not match or a specific method is passed. + # Check all for first match with matching host. If no matching + # host is found, go with first result. + first_match = None + + for rule in self.map._rules_by_endpoint.get(endpoint, ()): + if rule.suitable_for(values, method): + build_rv = rule.build(values, append_unknown) + + if build_rv is not None: + rv = (build_rv[0], build_rv[1], rule.websocket) + if self.map.host_matching: + if rv[0] == self.server_name: + return rv + elif first_match is None: + first_match = rv + else: + return rv + + return first_match + + def build( + self, + endpoint: str, + values: t.Optional[t.Mapping[str, t.Any]] = None, + method: t.Optional[str] = None, + force_external: bool = False, + append_unknown: bool = True, + url_scheme: t.Optional[str] = None, + ) -> str: + """Building URLs works pretty much the other way round. Instead of + `match` you call `build` and pass it the endpoint and a dict of + arguments for the placeholders. + + The `build` function also accepts an argument called `force_external` + which, if you set it to `True` will force external URLs. Per default + external URLs (include the server name) will only be used if the + target URL is on a different subdomain. + + >>> m = Map([ + ... Rule('/', endpoint='index'), + ... Rule('/downloads/', endpoint='downloads/index'), + ... Rule('/downloads/', endpoint='downloads/show') + ... ]) + >>> urls = m.bind("example.com", "/") + >>> urls.build("index", {}) + '/' + >>> urls.build("downloads/show", {'id': 42}) + '/downloads/42' + >>> urls.build("downloads/show", {'id': 42}, force_external=True) + 'http://example.com/downloads/42' + + Because URLs cannot contain non ASCII data you will always get + bytes back. Non ASCII characters are urlencoded with the + charset defined on the map instance. + + Additional values are converted to strings and appended to the URL as + URL querystring parameters: + + >>> urls.build("index", {'q': 'My Searchstring'}) + '/?q=My+Searchstring' + + When processing those additional values, lists are furthermore + interpreted as multiple values (as per + :py:class:`werkzeug.datastructures.MultiDict`): + + >>> urls.build("index", {'q': ['a', 'b', 'c']}) + '/?q=a&q=b&q=c' + + Passing a ``MultiDict`` will also add multiple values: + + >>> urls.build("index", MultiDict((('p', 'z'), ('q', 'a'), ('q', 'b')))) + '/?p=z&q=a&q=b' + + If a rule does not exist when building a `BuildError` exception is + raised. + + The build method accepts an argument called `method` which allows you + to specify the method you want to have an URL built for if you have + different methods for the same endpoint specified. + + :param endpoint: the endpoint of the URL to build. + :param values: the values for the URL to build. Unhandled values are + appended to the URL as query parameters. + :param method: the HTTP method for the rule if there are different + URLs for different methods on the same endpoint. + :param force_external: enforce full canonical external URLs. If the URL + scheme is not provided, this will generate + a protocol-relative URL. + :param append_unknown: unknown parameters are appended to the generated + URL as query string argument. Disable this + if you want the builder to ignore those. + :param url_scheme: Scheme to use in place of the bound + :attr:`url_scheme`. + + .. versionchanged:: 2.0 + Added the ``url_scheme`` parameter. + + .. versionadded:: 0.6 + Added the ``append_unknown`` parameter. + """ + self.map.update() + + if values: + temp_values: t.Dict[str, t.Union[t.List[t.Any], t.Any]] = {} + always_list = isinstance(values, MultiDict) + key: str + value: t.Optional[t.Union[t.List[t.Any], t.Any]] + + # For MultiDict, dict.items(values) is like values.lists() + # without the call or list coercion overhead. + for key, value in dict.items(values): # type: ignore + if value is None: + continue + + if always_list or isinstance(value, (list, tuple)): + value = [v for v in value if v is not None] + + if not value: + continue + + if len(value) == 1: + value = value[0] + + temp_values[key] = value + + values = temp_values + else: + values = {} + + rv = self._partial_build(endpoint, values, method, append_unknown) + if rv is None: + raise BuildError(endpoint, values, method, self) + + domain_part, path, websocket = rv + host = self.get_host(domain_part) + + if url_scheme is None: + url_scheme = self.url_scheme + + # Always build WebSocket routes with the scheme (browsers + # require full URLs). If bound to a WebSocket, ensure that HTTP + # routes are built with an HTTP scheme. + secure = url_scheme in {"https", "wss"} + + if websocket: + force_external = True + url_scheme = "wss" if secure else "ws" + elif url_scheme: + url_scheme = "https" if secure else "http" + + # shortcut this. + if not force_external and ( + (self.map.host_matching and host == self.server_name) + or (not self.map.host_matching and domain_part == self.subdomain) + ): + return f"{self.script_name.rstrip('/')}/{path.lstrip('/')}" + + scheme = f"{url_scheme}:" if url_scheme else "" + return f"{scheme}//{host}{self.script_name[:-1]}/{path.lstrip('/')}" diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__init__.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..a4aff47d4 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__pycache__/multipart.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__pycache__/multipart.cpython-38.pyc new file mode 100644 index 000000000..2a983b75c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__pycache__/multipart.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__pycache__/request.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__pycache__/request.cpython-38.pyc new file mode 100644 index 000000000..0e175b52c Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__pycache__/request.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__pycache__/response.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__pycache__/response.cpython-38.pyc new file mode 100644 index 000000000..d291f40c4 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__pycache__/response.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__pycache__/utils.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__pycache__/utils.cpython-38.pyc new file mode 100644 index 000000000..1eb81f3fb Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/__pycache__/utils.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/multipart.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/multipart.py new file mode 100644 index 000000000..bb8ab3455 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/multipart.py @@ -0,0 +1,260 @@ +import re +from dataclasses import dataclass +from enum import auto +from enum import Enum +from typing import cast +from typing import List +from typing import Optional +from typing import Tuple + +from .._internal import _to_bytes +from .._internal import _to_str +from ..datastructures import Headers +from ..exceptions import RequestEntityTooLarge +from ..http import parse_options_header + + +class Event: + pass + + +@dataclass(frozen=True) +class Preamble(Event): + data: bytes + + +@dataclass(frozen=True) +class Field(Event): + name: str + headers: Headers + + +@dataclass(frozen=True) +class File(Event): + name: str + filename: str + headers: Headers + + +@dataclass(frozen=True) +class Data(Event): + data: bytes + more_data: bool + + +@dataclass(frozen=True) +class Epilogue(Event): + data: bytes + + +class NeedData(Event): + pass + + +NEED_DATA = NeedData() + + +class State(Enum): + PREAMBLE = auto() + PART = auto() + DATA = auto() + EPILOGUE = auto() + COMPLETE = auto() + + +# Multipart line breaks MUST be CRLF (\r\n) by RFC-7578, except that +# many implementations break this and either use CR or LF alone. +LINE_BREAK = b"(?:\r\n|\n|\r)" +BLANK_LINE_RE = re.compile(b"(?:\r\n\r\n|\r\r|\n\n)", re.MULTILINE) +LINE_BREAK_RE = re.compile(LINE_BREAK, re.MULTILINE) +# Header values can be continued via a space or tab after the linebreak, as +# per RFC2231 +HEADER_CONTINUATION_RE = re.compile(b"%s[ \t]" % LINE_BREAK, re.MULTILINE) + + +class MultipartDecoder: + """Decodes a multipart message as bytes into Python events. + + The part data is returned as available to allow the caller to save + the data from memory to disk, if desired. + """ + + def __init__( + self, + boundary: bytes, + max_form_memory_size: Optional[int] = None, + ) -> None: + self.buffer = bytearray() + self.complete = False + self.max_form_memory_size = max_form_memory_size + self.state = State.PREAMBLE + self.boundary = boundary + + # Note in the below \h i.e. horizontal whitespace is used + # as [^\S\n\r] as \h isn't supported in python. + + # The preamble must end with a boundary where the boundary is + # prefixed by a line break, RFC2046. Except that many + # implementations including Werkzeug's tests omit the line + # break prefix. In addition the first boundary could be the + # epilogue boundary (for empty form-data) hence the matching + # group to understand if it is an epilogue boundary. + self.preamble_re = re.compile( + br"%s?--%s(--[^\S\n\r]*%s?|[^\S\n\r]*%s)" + % (LINE_BREAK, re.escape(boundary), LINE_BREAK, LINE_BREAK), + re.MULTILINE, + ) + # A boundary must include a line break prefix and suffix, and + # may include trailing whitespace. In addition the boundary + # could be the epilogue boundary hence the matching group to + # understand if it is an epilogue boundary. + self.boundary_re = re.compile( + br"%s--%s(--[^\S\n\r]*%s?|[^\S\n\r]*%s)" + % (LINE_BREAK, re.escape(boundary), LINE_BREAK, LINE_BREAK), + re.MULTILINE, + ) + + def last_newline(self) -> int: + try: + last_nl = self.buffer.rindex(b"\n") + except ValueError: + last_nl = len(self.buffer) + try: + last_cr = self.buffer.rindex(b"\r") + except ValueError: + last_cr = len(self.buffer) + + return min(last_nl, last_cr) + + def receive_data(self, data: Optional[bytes]) -> None: + if data is None: + self.complete = True + elif ( + self.max_form_memory_size is not None + and len(self.buffer) + len(data) > self.max_form_memory_size + ): + raise RequestEntityTooLarge() + else: + self.buffer.extend(data) + + def next_event(self) -> Event: + event: Event = NEED_DATA + + if self.state == State.PREAMBLE: + match = self.preamble_re.search(self.buffer) + if match is not None: + if match.group(1).startswith(b"--"): + self.state = State.EPILOGUE + else: + self.state = State.PART + data = bytes(self.buffer[: match.start()]) + del self.buffer[: match.end()] + event = Preamble(data=data) + + elif self.state == State.PART: + match = BLANK_LINE_RE.search(self.buffer) + if match is not None: + headers = self._parse_headers(self.buffer[: match.start()]) + del self.buffer[: match.end()] + + if "content-disposition" not in headers: + raise ValueError("Missing Content-Disposition header") + + disposition, extra = parse_options_header( + headers["content-disposition"] + ) + name = cast(str, extra.get("name")) + filename = extra.get("filename") + if filename is not None: + event = File( + filename=filename, + headers=headers, + name=name, + ) + else: + event = Field( + headers=headers, + name=name, + ) + self.state = State.DATA + + elif self.state == State.DATA: + if self.buffer.find(b"--" + self.boundary) == -1: + # No complete boundary in the buffer, but there may be + # a partial boundary at the end. As the boundary + # starts with either a nl or cr find the earliest and + # return up to that as data. + data_length = del_index = self.last_newline() + more_data = True + else: + match = self.boundary_re.search(self.buffer) + if match is not None: + if match.group(1).startswith(b"--"): + self.state = State.EPILOGUE + else: + self.state = State.PART + data_length = match.start() + del_index = match.end() + else: + data_length = del_index = self.last_newline() + more_data = match is None + + data = bytes(self.buffer[:data_length]) + del self.buffer[:del_index] + if data or not more_data: + event = Data(data=data, more_data=more_data) + + elif self.state == State.EPILOGUE and self.complete: + event = Epilogue(data=bytes(self.buffer)) + del self.buffer[:] + self.state = State.COMPLETE + + if self.complete and isinstance(event, NeedData): + raise ValueError(f"Invalid form-data cannot parse beyond {self.state}") + + return event + + def _parse_headers(self, data: bytes) -> Headers: + headers: List[Tuple[str, str]] = [] + # Merge the continued headers into one line + data = HEADER_CONTINUATION_RE.sub(b" ", data) + # Now there is one header per line + for line in data.splitlines(): + if line.strip() != b"": + name, value = _to_str(line).strip().split(":", 1) + headers.append((name.strip(), value.strip())) + return Headers(headers) + + +class MultipartEncoder: + def __init__(self, boundary: bytes) -> None: + self.boundary = boundary + self.state = State.PREAMBLE + + def send_event(self, event: Event) -> bytes: + if isinstance(event, Preamble) and self.state == State.PREAMBLE: + self.state = State.PART + return event.data + elif isinstance(event, (Field, File)) and self.state in { + State.PREAMBLE, + State.PART, + State.DATA, + }: + self.state = State.DATA + data = b"\r\n--" + self.boundary + b"\r\n" + data += b'Content-Disposition: form-data; name="%s"' % _to_bytes(event.name) + if isinstance(event, File): + data += b'; filename="%s"' % _to_bytes(event.filename) + data += b"\r\n" + for name, value in cast(Field, event).headers: + if name.lower() != "content-disposition": + data += _to_bytes(f"{name}: {value}\r\n") + data += b"\r\n" + return data + elif isinstance(event, Data) and self.state == State.DATA: + return event.data + elif isinstance(event, Epilogue): + self.state = State.COMPLETE + return b"\r\n--" + self.boundary + b"--\r\n" + event.data + else: + raise ValueError(f"Cannot generate {event} in state: {self.state}") diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/request.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/request.py new file mode 100644 index 000000000..2c21a2134 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/request.py @@ -0,0 +1,548 @@ +import typing as t +from datetime import datetime + +from .._internal import _to_str +from ..datastructures import Accept +from ..datastructures import Authorization +from ..datastructures import CharsetAccept +from ..datastructures import ETags +from ..datastructures import Headers +from ..datastructures import HeaderSet +from ..datastructures import IfRange +from ..datastructures import ImmutableList +from ..datastructures import ImmutableMultiDict +from ..datastructures import LanguageAccept +from ..datastructures import MIMEAccept +from ..datastructures import MultiDict +from ..datastructures import Range +from ..datastructures import RequestCacheControl +from ..http import parse_accept_header +from ..http import parse_authorization_header +from ..http import parse_cache_control_header +from ..http import parse_cookie +from ..http import parse_date +from ..http import parse_etags +from ..http import parse_if_range_header +from ..http import parse_list_header +from ..http import parse_options_header +from ..http import parse_range_header +from ..http import parse_set_header +from ..urls import url_decode +from ..user_agent import UserAgent +from ..useragents import _UserAgent as _DeprecatedUserAgent +from ..utils import cached_property +from ..utils import header_property +from .utils import get_current_url +from .utils import get_host + + +class Request: + """Represents the non-IO parts of a HTTP request, including the + method, URL info, and headers. + + This class is not meant for general use. It should only be used when + implementing WSGI, ASGI, or another HTTP application spec. Werkzeug + provides a WSGI implementation at :cls:`werkzeug.wrappers.Request`. + + :param method: The method the request was made with, such as + ``GET``. + :param scheme: The URL scheme of the protocol the request used, such + as ``https`` or ``wss``. + :param server: The address of the server. ``(host, port)``, + ``(path, None)`` for unix sockets, or ``None`` if not known. + :param root_path: The prefix that the application is mounted under. + This is prepended to generated URLs, but is not part of route + matching. + :param path: The path part of the URL after ``root_path``. + :param query_string: The part of the URL after the "?". + :param headers: The headers received with the request. + :param remote_addr: The address of the client sending the request. + + .. versionadded:: 2.0 + """ + + #: The charset used to decode most data in the request. + charset = "utf-8" + + #: the error handling procedure for errors, defaults to 'replace' + encoding_errors = "replace" + + #: the class to use for `args` and `form`. The default is an + #: :class:`~werkzeug.datastructures.ImmutableMultiDict` which supports + #: multiple values per key. alternatively it makes sense to use an + #: :class:`~werkzeug.datastructures.ImmutableOrderedMultiDict` which + #: preserves order or a :class:`~werkzeug.datastructures.ImmutableDict` + #: which is the fastest but only remembers the last key. It is also + #: possible to use mutable structures, but this is not recommended. + #: + #: .. versionadded:: 0.6 + parameter_storage_class: t.Type[MultiDict] = ImmutableMultiDict + + #: The type to be used for dict values from the incoming WSGI + #: environment. (For example for :attr:`cookies`.) By default an + #: :class:`~werkzeug.datastructures.ImmutableMultiDict` is used. + #: + #: .. versionchanged:: 1.0.0 + #: Changed to ``ImmutableMultiDict`` to support multiple values. + #: + #: .. versionadded:: 0.6 + dict_storage_class: t.Type[MultiDict] = ImmutableMultiDict + + #: the type to be used for list values from the incoming WSGI environment. + #: By default an :class:`~werkzeug.datastructures.ImmutableList` is used + #: (for example for :attr:`access_list`). + #: + #: .. versionadded:: 0.6 + list_storage_class: t.Type[t.List] = ImmutableList + + user_agent_class = _DeprecatedUserAgent + """The class used and returned by the :attr:`user_agent` property to + parse the header. Defaults to + :class:`~werkzeug.user_agent.UserAgent`, which does no parsing. An + extension can provide a subclass that uses a parser to provide other + data. + + .. versionadded:: 2.0 + """ + + #: Valid host names when handling requests. By default all hosts are + #: trusted, which means that whatever the client says the host is + #: will be accepted. + #: + #: Because ``Host`` and ``X-Forwarded-Host`` headers can be set to + #: any value by a malicious client, it is recommended to either set + #: this property or implement similar validation in the proxy (if + #: the application is being run behind one). + #: + #: .. versionadded:: 0.9 + trusted_hosts: t.Optional[t.List[str]] = None + + def __init__( + self, + method: str, + scheme: str, + server: t.Optional[t.Tuple[str, t.Optional[int]]], + root_path: str, + path: str, + query_string: bytes, + headers: Headers, + remote_addr: t.Optional[str], + ) -> None: + #: The method the request was made with, such as ``GET``. + self.method = method.upper() + #: The URL scheme of the protocol the request used, such as + #: ``https`` or ``wss``. + self.scheme = scheme + #: The address of the server. ``(host, port)``, ``(path, None)`` + #: for unix sockets, or ``None`` if not known. + self.server = server + #: The prefix that the application is mounted under, without a + #: trailing slash. :attr:`path` comes after this. + self.root_path = root_path.rstrip("/") + #: The path part of the URL after :attr:`root_path`. This is the + #: path used for routing within the application. + self.path = "/" + path.lstrip("/") + #: The part of the URL after the "?". This is the raw value, use + #: :attr:`args` for the parsed values. + self.query_string = query_string + #: The headers received with the request. + self.headers = headers + #: The address of the client sending the request. + self.remote_addr = remote_addr + + def __repr__(self) -> str: + try: + url = self.url + except Exception as e: + url = f"(invalid URL: {e})" + + return f"<{type(self).__name__} {url!r} [{self.method}]>" + + @property + def url_charset(self) -> str: + """The charset that is assumed for URLs. Defaults to the value + of :attr:`charset`. + + .. versionadded:: 0.6 + """ + return self.charset + + @cached_property + def args(self) -> "MultiDict[str, str]": + """The parsed URL parameters (the part in the URL after the question + mark). + + By default an + :class:`~werkzeug.datastructures.ImmutableMultiDict` + is returned from this function. This can be changed by setting + :attr:`parameter_storage_class` to a different type. This might + be necessary if the order of the form data is important. + """ + return url_decode( + self.query_string, + self.url_charset, + errors=self.encoding_errors, + cls=self.parameter_storage_class, + ) + + @cached_property + def access_route(self) -> t.List[str]: + """If a forwarded header exists this is a list of all ip addresses + from the client ip to the last proxy server. + """ + if "X-Forwarded-For" in self.headers: + return self.list_storage_class( + parse_list_header(self.headers["X-Forwarded-For"]) + ) + elif self.remote_addr is not None: + return self.list_storage_class([self.remote_addr]) + return self.list_storage_class() + + @cached_property + def full_path(self) -> str: + """Requested path, including the query string.""" + return f"{self.path}?{_to_str(self.query_string, self.url_charset)}" + + @property + def is_secure(self) -> bool: + """``True`` if the request was made with a secure protocol + (HTTPS or WSS). + """ + return self.scheme in {"https", "wss"} + + @cached_property + def url(self) -> str: + """The full request URL with the scheme, host, root path, path, + and query string.""" + return get_current_url( + self.scheme, self.host, self.root_path, self.path, self.query_string + ) + + @cached_property + def base_url(self) -> str: + """Like :attr:`url` but without the query string.""" + return get_current_url(self.scheme, self.host, self.root_path, self.path) + + @cached_property + def root_url(self) -> str: + """The request URL scheme, host, and root path. This is the root + that the application is accessed from. + """ + return get_current_url(self.scheme, self.host, self.root_path) + + @cached_property + def host_url(self) -> str: + """The request URL scheme and host only.""" + return get_current_url(self.scheme, self.host) + + @cached_property + def host(self) -> str: + """The host name the request was made to, including the port if + it's non-standard. Validated with :attr:`trusted_hosts`. + """ + return get_host( + self.scheme, self.headers.get("host"), self.server, self.trusted_hosts + ) + + @cached_property + def cookies(self) -> "ImmutableMultiDict[str, str]": + """A :class:`dict` with the contents of all cookies transmitted with + the request.""" + wsgi_combined_cookie = ";".join(self.headers.getlist("Cookie")) + return parse_cookie( # type: ignore + wsgi_combined_cookie, + self.charset, + self.encoding_errors, + cls=self.dict_storage_class, + ) + + # Common Descriptors + + content_type = header_property[str]( + "Content-Type", + doc="""The Content-Type entity-header field indicates the media + type of the entity-body sent to the recipient or, in the case of + the HEAD method, the media type that would have been sent had + the request been a GET.""", + read_only=True, + ) + + @cached_property + def content_length(self) -> t.Optional[int]: + """The Content-Length entity-header field indicates the size of the + entity-body in bytes or, in the case of the HEAD method, the size of + the entity-body that would have been sent had the request been a + GET. + """ + if self.headers.get("Transfer-Encoding", "") == "chunked": + return None + + content_length = self.headers.get("Content-Length") + if content_length is not None: + try: + return max(0, int(content_length)) + except (ValueError, TypeError): + pass + + return None + + content_encoding = header_property[str]( + "Content-Encoding", + doc="""The Content-Encoding entity-header field is used as a + modifier to the media-type. When present, its value indicates + what additional content codings have been applied to the + entity-body, and thus what decoding mechanisms must be applied + in order to obtain the media-type referenced by the Content-Type + header field. + + .. versionadded:: 0.9""", + read_only=True, + ) + content_md5 = header_property[str]( + "Content-MD5", + doc="""The Content-MD5 entity-header field, as defined in + RFC 1864, is an MD5 digest of the entity-body for the purpose of + providing an end-to-end message integrity check (MIC) of the + entity-body. (Note: a MIC is good for detecting accidental + modification of the entity-body in transit, but is not proof + against malicious attacks.) + + .. versionadded:: 0.9""", + read_only=True, + ) + referrer = header_property[str]( + "Referer", + doc="""The Referer[sic] request-header field allows the client + to specify, for the server's benefit, the address (URI) of the + resource from which the Request-URI was obtained (the + "referrer", although the header field is misspelled).""", + read_only=True, + ) + date = header_property( + "Date", + None, + parse_date, + doc="""The Date general-header field represents the date and + time at which the message was originated, having the same + semantics as orig-date in RFC 822. + + .. versionchanged:: 2.0 + The datetime object is timezone-aware. + """, + read_only=True, + ) + max_forwards = header_property( + "Max-Forwards", + None, + int, + doc="""The Max-Forwards request-header field provides a + mechanism with the TRACE and OPTIONS methods to limit the number + of proxies or gateways that can forward the request to the next + inbound server.""", + read_only=True, + ) + + def _parse_content_type(self) -> None: + if not hasattr(self, "_parsed_content_type"): + self._parsed_content_type = parse_options_header( + self.headers.get("Content-Type", "") + ) + + @property + def mimetype(self) -> str: + """Like :attr:`content_type`, but without parameters (eg, without + charset, type etc.) and always lowercase. For example if the content + type is ``text/HTML; charset=utf-8`` the mimetype would be + ``'text/html'``. + """ + self._parse_content_type() + return self._parsed_content_type[0].lower() + + @property + def mimetype_params(self) -> t.Dict[str, str]: + """The mimetype parameters as dict. For example if the content + type is ``text/html; charset=utf-8`` the params would be + ``{'charset': 'utf-8'}``. + """ + self._parse_content_type() + return self._parsed_content_type[1] + + @cached_property + def pragma(self) -> HeaderSet: + """The Pragma general-header field is used to include + implementation-specific directives that might apply to any recipient + along the request/response chain. All pragma directives specify + optional behavior from the viewpoint of the protocol; however, some + systems MAY require that behavior be consistent with the directives. + """ + return parse_set_header(self.headers.get("Pragma", "")) + + # Accept + + @cached_property + def accept_mimetypes(self) -> MIMEAccept: + """List of mimetypes this client supports as + :class:`~werkzeug.datastructures.MIMEAccept` object. + """ + return parse_accept_header(self.headers.get("Accept"), MIMEAccept) + + @cached_property + def accept_charsets(self) -> CharsetAccept: + """List of charsets this client supports as + :class:`~werkzeug.datastructures.CharsetAccept` object. + """ + return parse_accept_header(self.headers.get("Accept-Charset"), CharsetAccept) + + @cached_property + def accept_encodings(self) -> Accept: + """List of encodings this client accepts. Encodings in a HTTP term + are compression encodings such as gzip. For charsets have a look at + :attr:`accept_charset`. + """ + return parse_accept_header(self.headers.get("Accept-Encoding")) + + @cached_property + def accept_languages(self) -> LanguageAccept: + """List of languages this client accepts as + :class:`~werkzeug.datastructures.LanguageAccept` object. + + .. versionchanged 0.5 + In previous versions this was a regular + :class:`~werkzeug.datastructures.Accept` object. + """ + return parse_accept_header(self.headers.get("Accept-Language"), LanguageAccept) + + # ETag + + @cached_property + def cache_control(self) -> RequestCacheControl: + """A :class:`~werkzeug.datastructures.RequestCacheControl` object + for the incoming cache control headers. + """ + cache_control = self.headers.get("Cache-Control") + return parse_cache_control_header(cache_control, None, RequestCacheControl) + + @cached_property + def if_match(self) -> ETags: + """An object containing all the etags in the `If-Match` header. + + :rtype: :class:`~werkzeug.datastructures.ETags` + """ + return parse_etags(self.headers.get("If-Match")) + + @cached_property + def if_none_match(self) -> ETags: + """An object containing all the etags in the `If-None-Match` header. + + :rtype: :class:`~werkzeug.datastructures.ETags` + """ + return parse_etags(self.headers.get("If-None-Match")) + + @cached_property + def if_modified_since(self) -> t.Optional[datetime]: + """The parsed `If-Modified-Since` header as a datetime object. + + .. versionchanged:: 2.0 + The datetime object is timezone-aware. + """ + return parse_date(self.headers.get("If-Modified-Since")) + + @cached_property + def if_unmodified_since(self) -> t.Optional[datetime]: + """The parsed `If-Unmodified-Since` header as a datetime object. + + .. versionchanged:: 2.0 + The datetime object is timezone-aware. + """ + return parse_date(self.headers.get("If-Unmodified-Since")) + + @cached_property + def if_range(self) -> IfRange: + """The parsed ``If-Range`` header. + + .. versionchanged:: 2.0 + ``IfRange.date`` is timezone-aware. + + .. versionadded:: 0.7 + """ + return parse_if_range_header(self.headers.get("If-Range")) + + @cached_property + def range(self) -> t.Optional[Range]: + """The parsed `Range` header. + + .. versionadded:: 0.7 + + :rtype: :class:`~werkzeug.datastructures.Range` + """ + return parse_range_header(self.headers.get("Range")) + + # User Agent + + @cached_property + def user_agent(self) -> UserAgent: + """The user agent. Use ``user_agent.string`` to get the header + value. Set :attr:`user_agent_class` to a subclass of + :class:`~werkzeug.user_agent.UserAgent` to provide parsing for + the other properties or other extended data. + + .. versionchanged:: 2.0 + The built in parser is deprecated and will be removed in + Werkzeug 2.1. A ``UserAgent`` subclass must be set to parse + data from the string. + """ + return self.user_agent_class(self.headers.get("User-Agent", "")) + + # Authorization + + @cached_property + def authorization(self) -> t.Optional[Authorization]: + """The `Authorization` object in parsed form.""" + return parse_authorization_header(self.headers.get("Authorization")) + + # CORS + + origin = header_property[str]( + "Origin", + doc=( + "The host that the request originated from. Set" + " :attr:`~CORSResponseMixin.access_control_allow_origin` on" + " the response to indicate which origins are allowed." + ), + read_only=True, + ) + + access_control_request_headers = header_property( + "Access-Control-Request-Headers", + load_func=parse_set_header, + doc=( + "Sent with a preflight request to indicate which headers" + " will be sent with the cross origin request. Set" + " :attr:`~CORSResponseMixin.access_control_allow_headers`" + " on the response to indicate which headers are allowed." + ), + read_only=True, + ) + + access_control_request_method = header_property[str]( + "Access-Control-Request-Method", + doc=( + "Sent with a preflight request to indicate which method" + " will be used for the cross origin request. Set" + " :attr:`~CORSResponseMixin.access_control_allow_methods`" + " on the response to indicate which methods are allowed." + ), + read_only=True, + ) + + @property + def is_json(self) -> bool: + """Check if the mimetype indicates JSON data, either + :mimetype:`application/json` or :mimetype:`application/*+json`. + """ + mt = self.mimetype + return ( + mt == "application/json" + or mt.startswith("application/") + and mt.endswith("+json") + ) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/response.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/response.py new file mode 100644 index 000000000..82817e8c1 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/response.py @@ -0,0 +1,704 @@ +import typing as t +from datetime import datetime +from datetime import timedelta +from datetime import timezone +from http import HTTPStatus + +from .._internal import _to_str +from ..datastructures import Headers +from ..datastructures import HeaderSet +from ..http import dump_cookie +from ..http import HTTP_STATUS_CODES +from ..utils import get_content_type +from werkzeug.datastructures import CallbackDict +from werkzeug.datastructures import ContentRange +from werkzeug.datastructures import ContentSecurityPolicy +from werkzeug.datastructures import ResponseCacheControl +from werkzeug.datastructures import WWWAuthenticate +from werkzeug.http import COEP +from werkzeug.http import COOP +from werkzeug.http import dump_age +from werkzeug.http import dump_header +from werkzeug.http import dump_options_header +from werkzeug.http import http_date +from werkzeug.http import parse_age +from werkzeug.http import parse_cache_control_header +from werkzeug.http import parse_content_range_header +from werkzeug.http import parse_csp_header +from werkzeug.http import parse_date +from werkzeug.http import parse_options_header +from werkzeug.http import parse_set_header +from werkzeug.http import parse_www_authenticate_header +from werkzeug.http import quote_etag +from werkzeug.http import unquote_etag +from werkzeug.utils import header_property + + +def _set_property(name: str, doc: t.Optional[str] = None) -> property: + def fget(self: "Response") -> HeaderSet: + def on_update(header_set: HeaderSet) -> None: + if not header_set and name in self.headers: + del self.headers[name] + elif header_set: + self.headers[name] = header_set.to_header() + + return parse_set_header(self.headers.get(name), on_update) + + def fset( + self: "Response", + value: t.Optional[ + t.Union[str, t.Dict[str, t.Union[str, int]], t.Iterable[str]] + ], + ) -> None: + if not value: + del self.headers[name] + elif isinstance(value, str): + self.headers[name] = value + else: + self.headers[name] = dump_header(value) + + return property(fget, fset, doc=doc) + + +class Response: + """Represents the non-IO parts of an HTTP response, specifically the + status and headers but not the body. + + This class is not meant for general use. It should only be used when + implementing WSGI, ASGI, or another HTTP application spec. Werkzeug + provides a WSGI implementation at :cls:`werkzeug.wrappers.Response`. + + :param status: The status code for the response. Either an int, in + which case the default status message is added, or a string in + the form ``{code} {message}``, like ``404 Not Found``. Defaults + to 200. + :param headers: A :class:`~werkzeug.datastructures.Headers` object, + or a list of ``(key, value)`` tuples that will be converted to a + ``Headers`` object. + :param mimetype: The mime type (content type without charset or + other parameters) of the response. If the value starts with + ``text/`` (or matches some other special cases), the charset + will be added to create the ``content_type``. + :param content_type: The full content type of the response. + Overrides building the value from ``mimetype``. + + .. versionadded:: 2.0 + """ + + #: the charset of the response. + charset = "utf-8" + + #: the default status if none is provided. + default_status = 200 + + #: the default mimetype if none is provided. + default_mimetype = "text/plain" + + #: Warn if a cookie header exceeds this size. The default, 4093, should be + #: safely `supported by most browsers `_. A cookie larger than + #: this size will still be sent, but it may be ignored or handled + #: incorrectly by some browsers. Set to 0 to disable this check. + #: + #: .. versionadded:: 0.13 + #: + #: .. _`cookie`: http://browsercookielimits.squawky.net/ + max_cookie_size = 4093 + + # A :class:`Headers` object representing the response headers. + headers: Headers + + def __init__( + self, + status: t.Optional[t.Union[int, str, HTTPStatus]] = None, + headers: t.Optional[ + t.Union[ + t.Mapping[str, t.Union[str, int, t.Iterable[t.Union[str, int]]]], + t.Iterable[t.Tuple[str, t.Union[str, int]]], + ] + ] = None, + mimetype: t.Optional[str] = None, + content_type: t.Optional[str] = None, + ) -> None: + if isinstance(headers, Headers): + self.headers = headers + elif not headers: + self.headers = Headers() + else: + self.headers = Headers(headers) + + if content_type is None: + if mimetype is None and "content-type" not in self.headers: + mimetype = self.default_mimetype + if mimetype is not None: + mimetype = get_content_type(mimetype, self.charset) + content_type = mimetype + if content_type is not None: + self.headers["Content-Type"] = content_type + if status is None: + status = self.default_status + self.status = status # type: ignore + + def __repr__(self) -> str: + return f"<{type(self).__name__} [{self.status}]>" + + @property + def status_code(self) -> int: + """The HTTP status code as a number.""" + return self._status_code + + @status_code.setter + def status_code(self, code: int) -> None: + self.status = code # type: ignore + + @property + def status(self) -> str: + """The HTTP status code as a string.""" + return self._status + + @status.setter + def status(self, value: t.Union[str, int, HTTPStatus]) -> None: + if not isinstance(value, (str, bytes, int, HTTPStatus)): + raise TypeError("Invalid status argument") + + self._status, self._status_code = self._clean_status(value) + + def _clean_status(self, value: t.Union[str, int, HTTPStatus]) -> t.Tuple[str, int]: + if isinstance(value, HTTPStatus): + value = int(value) + status = _to_str(value, self.charset) + split_status = status.split(None, 1) + + if len(split_status) == 0: + raise ValueError("Empty status argument") + + if len(split_status) > 1: + if split_status[0].isdigit(): + # code and message + return status, int(split_status[0]) + + # multi-word message + return f"0 {status}", 0 + + if split_status[0].isdigit(): + # code only + status_code = int(split_status[0]) + + try: + status = f"{status_code} {HTTP_STATUS_CODES[status_code].upper()}" + except KeyError: + status = f"{status_code} UNKNOWN" + + return status, status_code + + # one-word message + return f"0 {status}", 0 + + def set_cookie( + self, + key: str, + value: str = "", + max_age: t.Optional[t.Union[timedelta, int]] = None, + expires: t.Optional[t.Union[str, datetime, int, float]] = None, + path: t.Optional[str] = "/", + domain: t.Optional[str] = None, + secure: bool = False, + httponly: bool = False, + samesite: t.Optional[str] = None, + ) -> None: + """Sets a cookie. + + A warning is raised if the size of the cookie header exceeds + :attr:`max_cookie_size`, but the header will still be set. + + :param key: the key (name) of the cookie to be set. + :param value: the value of the cookie. + :param max_age: should be a number of seconds, or `None` (default) if + the cookie should last only as long as the client's + browser session. + :param expires: should be a `datetime` object or UNIX timestamp. + :param path: limits the cookie to a given path, per default it will + span the whole domain. + :param domain: if you want to set a cross-domain cookie. For example, + ``domain=".example.com"`` will set a cookie that is + readable by the domain ``www.example.com``, + ``foo.example.com`` etc. Otherwise, a cookie will only + be readable by the domain that set it. + :param secure: If ``True``, the cookie will only be available + via HTTPS. + :param httponly: Disallow JavaScript access to the cookie. + :param samesite: Limit the scope of the cookie to only be + attached to requests that are "same-site". + """ + self.headers.add( + "Set-Cookie", + dump_cookie( + key, + value=value, + max_age=max_age, + expires=expires, + path=path, + domain=domain, + secure=secure, + httponly=httponly, + charset=self.charset, + max_size=self.max_cookie_size, + samesite=samesite, + ), + ) + + def delete_cookie( + self, + key: str, + path: str = "/", + domain: t.Optional[str] = None, + secure: bool = False, + httponly: bool = False, + samesite: t.Optional[str] = None, + ) -> None: + """Delete a cookie. Fails silently if key doesn't exist. + + :param key: the key (name) of the cookie to be deleted. + :param path: if the cookie that should be deleted was limited to a + path, the path has to be defined here. + :param domain: if the cookie that should be deleted was limited to a + domain, that domain has to be defined here. + :param secure: If ``True``, the cookie will only be available + via HTTPS. + :param httponly: Disallow JavaScript access to the cookie. + :param samesite: Limit the scope of the cookie to only be + attached to requests that are "same-site". + """ + self.set_cookie( + key, + expires=0, + max_age=0, + path=path, + domain=domain, + secure=secure, + httponly=httponly, + samesite=samesite, + ) + + @property + def is_json(self) -> bool: + """Check if the mimetype indicates JSON data, either + :mimetype:`application/json` or :mimetype:`application/*+json`. + """ + mt = self.mimetype + return mt is not None and ( + mt == "application/json" + or mt.startswith("application/") + and mt.endswith("+json") + ) + + # Common Descriptors + + @property + def mimetype(self) -> t.Optional[str]: + """The mimetype (content type without charset etc.)""" + ct = self.headers.get("content-type") + + if ct: + return ct.split(";")[0].strip() + else: + return None + + @mimetype.setter + def mimetype(self, value: str) -> None: + self.headers["Content-Type"] = get_content_type(value, self.charset) + + @property + def mimetype_params(self) -> t.Dict[str, str]: + """The mimetype parameters as dict. For example if the + content type is ``text/html; charset=utf-8`` the params would be + ``{'charset': 'utf-8'}``. + + .. versionadded:: 0.5 + """ + + def on_update(d: CallbackDict) -> None: + self.headers["Content-Type"] = dump_options_header(self.mimetype, d) + + d = parse_options_header(self.headers.get("content-type", ""))[1] + return CallbackDict(d, on_update) + + location = header_property[str]( + "Location", + doc="""The Location response-header field is used to redirect + the recipient to a location other than the Request-URI for + completion of the request or identification of a new + resource.""", + ) + age = header_property( + "Age", + None, + parse_age, + dump_age, # type: ignore + doc="""The Age response-header field conveys the sender's + estimate of the amount of time since the response (or its + revalidation) was generated at the origin server. + + Age values are non-negative decimal integers, representing time + in seconds.""", + ) + content_type = header_property[str]( + "Content-Type", + doc="""The Content-Type entity-header field indicates the media + type of the entity-body sent to the recipient or, in the case of + the HEAD method, the media type that would have been sent had + the request been a GET.""", + ) + content_length = header_property( + "Content-Length", + None, + int, + str, + doc="""The Content-Length entity-header field indicates the size + of the entity-body, in decimal number of OCTETs, sent to the + recipient or, in the case of the HEAD method, the size of the + entity-body that would have been sent had the request been a + GET.""", + ) + content_location = header_property[str]( + "Content-Location", + doc="""The Content-Location entity-header field MAY be used to + supply the resource location for the entity enclosed in the + message when that entity is accessible from a location separate + from the requested resource's URI.""", + ) + content_encoding = header_property[str]( + "Content-Encoding", + doc="""The Content-Encoding entity-header field is used as a + modifier to the media-type. When present, its value indicates + what additional content codings have been applied to the + entity-body, and thus what decoding mechanisms must be applied + in order to obtain the media-type referenced by the Content-Type + header field.""", + ) + content_md5 = header_property[str]( + "Content-MD5", + doc="""The Content-MD5 entity-header field, as defined in + RFC 1864, is an MD5 digest of the entity-body for the purpose of + providing an end-to-end message integrity check (MIC) of the + entity-body. (Note: a MIC is good for detecting accidental + modification of the entity-body in transit, but is not proof + against malicious attacks.)""", + ) + date = header_property( + "Date", + None, + parse_date, + http_date, + doc="""The Date general-header field represents the date and + time at which the message was originated, having the same + semantics as orig-date in RFC 822. + + .. versionchanged:: 2.0 + The datetime object is timezone-aware. + """, + ) + expires = header_property( + "Expires", + None, + parse_date, + http_date, + doc="""The Expires entity-header field gives the date/time after + which the response is considered stale. A stale cache entry may + not normally be returned by a cache. + + .. versionchanged:: 2.0 + The datetime object is timezone-aware. + """, + ) + last_modified = header_property( + "Last-Modified", + None, + parse_date, + http_date, + doc="""The Last-Modified entity-header field indicates the date + and time at which the origin server believes the variant was + last modified. + + .. versionchanged:: 2.0 + The datetime object is timezone-aware. + """, + ) + + @property + def retry_after(self) -> t.Optional[datetime]: + """The Retry-After response-header field can be used with a + 503 (Service Unavailable) response to indicate how long the + service is expected to be unavailable to the requesting client. + + Time in seconds until expiration or date. + + .. versionchanged:: 2.0 + The datetime object is timezone-aware. + """ + value = self.headers.get("retry-after") + if value is None: + return None + elif value.isdigit(): + return datetime.now(timezone.utc) + timedelta(seconds=int(value)) + return parse_date(value) + + @retry_after.setter + def retry_after(self, value: t.Optional[t.Union[datetime, int, str]]) -> None: + if value is None: + if "retry-after" in self.headers: + del self.headers["retry-after"] + return + elif isinstance(value, datetime): + value = http_date(value) + else: + value = str(value) + self.headers["Retry-After"] = value + + vary = _set_property( + "Vary", + doc="""The Vary field value indicates the set of request-header + fields that fully determines, while the response is fresh, + whether a cache is permitted to use the response to reply to a + subsequent request without revalidation.""", + ) + content_language = _set_property( + "Content-Language", + doc="""The Content-Language entity-header field describes the + natural language(s) of the intended audience for the enclosed + entity. Note that this might not be equivalent to all the + languages used within the entity-body.""", + ) + allow = _set_property( + "Allow", + doc="""The Allow entity-header field lists the set of methods + supported by the resource identified by the Request-URI. The + purpose of this field is strictly to inform the recipient of + valid methods associated with the resource. An Allow header + field MUST be present in a 405 (Method Not Allowed) + response.""", + ) + + # ETag + + @property + def cache_control(self) -> ResponseCacheControl: + """The Cache-Control general-header field is used to specify + directives that MUST be obeyed by all caching mechanisms along the + request/response chain. + """ + + def on_update(cache_control: ResponseCacheControl) -> None: + if not cache_control and "cache-control" in self.headers: + del self.headers["cache-control"] + elif cache_control: + self.headers["Cache-Control"] = cache_control.to_header() + + return parse_cache_control_header( + self.headers.get("cache-control"), on_update, ResponseCacheControl + ) + + def set_etag(self, etag: str, weak: bool = False) -> None: + """Set the etag, and override the old one if there was one.""" + self.headers["ETag"] = quote_etag(etag, weak) + + def get_etag(self) -> t.Union[t.Tuple[str, bool], t.Tuple[None, None]]: + """Return a tuple in the form ``(etag, is_weak)``. If there is no + ETag the return value is ``(None, None)``. + """ + return unquote_etag(self.headers.get("ETag")) + + accept_ranges = header_property[str]( + "Accept-Ranges", + doc="""The `Accept-Ranges` header. Even though the name would + indicate that multiple values are supported, it must be one + string token only. + + The values ``'bytes'`` and ``'none'`` are common. + + .. versionadded:: 0.7""", + ) + + @property + def content_range(self) -> ContentRange: + """The ``Content-Range`` header as a + :class:`~werkzeug.datastructures.ContentRange` object. Available + even if the header is not set. + + .. versionadded:: 0.7 + """ + + def on_update(rng: ContentRange) -> None: + if not rng: + del self.headers["content-range"] + else: + self.headers["Content-Range"] = rng.to_header() + + rv = parse_content_range_header(self.headers.get("content-range"), on_update) + # always provide a content range object to make the descriptor + # more user friendly. It provides an unset() method that can be + # used to remove the header quickly. + if rv is None: + rv = ContentRange(None, None, None, on_update=on_update) + return rv + + @content_range.setter + def content_range(self, value: t.Optional[t.Union[ContentRange, str]]) -> None: + if not value: + del self.headers["content-range"] + elif isinstance(value, str): + self.headers["Content-Range"] = value + else: + self.headers["Content-Range"] = value.to_header() + + # Authorization + + @property + def www_authenticate(self) -> WWWAuthenticate: + """The ``WWW-Authenticate`` header in a parsed form.""" + + def on_update(www_auth: WWWAuthenticate) -> None: + if not www_auth and "www-authenticate" in self.headers: + del self.headers["www-authenticate"] + elif www_auth: + self.headers["WWW-Authenticate"] = www_auth.to_header() + + header = self.headers.get("www-authenticate") + return parse_www_authenticate_header(header, on_update) + + # CSP + + @property + def content_security_policy(self) -> ContentSecurityPolicy: + """The ``Content-Security-Policy`` header as a + :class:`~werkzeug.datastructures.ContentSecurityPolicy` object. Available + even if the header is not set. + + The Content-Security-Policy header adds an additional layer of + security to help detect and mitigate certain types of attacks. + """ + + def on_update(csp: ContentSecurityPolicy) -> None: + if not csp: + del self.headers["content-security-policy"] + else: + self.headers["Content-Security-Policy"] = csp.to_header() + + rv = parse_csp_header(self.headers.get("content-security-policy"), on_update) + if rv is None: + rv = ContentSecurityPolicy(None, on_update=on_update) + return rv + + @content_security_policy.setter + def content_security_policy( + self, value: t.Optional[t.Union[ContentSecurityPolicy, str]] + ) -> None: + if not value: + del self.headers["content-security-policy"] + elif isinstance(value, str): + self.headers["Content-Security-Policy"] = value + else: + self.headers["Content-Security-Policy"] = value.to_header() + + @property + def content_security_policy_report_only(self) -> ContentSecurityPolicy: + """The ``Content-Security-policy-report-only`` header as a + :class:`~werkzeug.datastructures.ContentSecurityPolicy` object. Available + even if the header is not set. + + The Content-Security-Policy-Report-Only header adds a csp policy + that is not enforced but is reported thereby helping detect + certain types of attacks. + """ + + def on_update(csp: ContentSecurityPolicy) -> None: + if not csp: + del self.headers["content-security-policy-report-only"] + else: + self.headers["Content-Security-policy-report-only"] = csp.to_header() + + rv = parse_csp_header( + self.headers.get("content-security-policy-report-only"), on_update + ) + if rv is None: + rv = ContentSecurityPolicy(None, on_update=on_update) + return rv + + @content_security_policy_report_only.setter + def content_security_policy_report_only( + self, value: t.Optional[t.Union[ContentSecurityPolicy, str]] + ) -> None: + if not value: + del self.headers["content-security-policy-report-only"] + elif isinstance(value, str): + self.headers["Content-Security-policy-report-only"] = value + else: + self.headers["Content-Security-policy-report-only"] = value.to_header() + + # CORS + + @property + def access_control_allow_credentials(self) -> bool: + """Whether credentials can be shared by the browser to + JavaScript code. As part of the preflight request it indicates + whether credentials can be used on the cross origin request. + """ + return "Access-Control-Allow-Credentials" in self.headers + + @access_control_allow_credentials.setter + def access_control_allow_credentials(self, value: t.Optional[bool]) -> None: + if value is True: + self.headers["Access-Control-Allow-Credentials"] = "true" + else: + self.headers.pop("Access-Control-Allow-Credentials", None) + + access_control_allow_headers = header_property( + "Access-Control-Allow-Headers", + load_func=parse_set_header, + dump_func=dump_header, + doc="Which headers can be sent with the cross origin request.", + ) + + access_control_allow_methods = header_property( + "Access-Control-Allow-Methods", + load_func=parse_set_header, + dump_func=dump_header, + doc="Which methods can be used for the cross origin request.", + ) + + access_control_allow_origin = header_property[str]( + "Access-Control-Allow-Origin", + doc="The origin or '*' for any origin that may make cross origin requests.", + ) + + access_control_expose_headers = header_property( + "Access-Control-Expose-Headers", + load_func=parse_set_header, + dump_func=dump_header, + doc="Which headers can be shared by the browser to JavaScript code.", + ) + + access_control_max_age = header_property( + "Access-Control-Max-Age", + load_func=int, + dump_func=str, + doc="The maximum age in seconds the access control settings can be cached for.", + ) + + cross_origin_opener_policy = header_property[COOP]( + "Cross-Origin-Opener-Policy", + load_func=lambda value: COOP(value), + dump_func=lambda value: value.value, + default=COOP.UNSAFE_NONE, + doc="""Allows control over sharing of browsing context group with cross-origin + documents. Values must be a member of the :class:`werkzeug.http.COOP` enum.""", + ) + + cross_origin_embedder_policy = header_property[COEP]( + "Cross-Origin-Embedder-Policy", + load_func=lambda value: COEP(value), + dump_func=lambda value: value.value, + default=COEP.UNSAFE_NONE, + doc="""Prevents a document from loading any cross-origin resources that do not + explicitly grant the document permission. Values must be a member of the + :class:`werkzeug.http.COEP` enum.""", + ) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/utils.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/utils.py new file mode 100644 index 000000000..1b4d8920c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/sansio/utils.py @@ -0,0 +1,142 @@ +import typing as t + +from .._internal import _encode_idna +from ..exceptions import SecurityError +from ..urls import uri_to_iri +from ..urls import url_quote + + +def host_is_trusted(hostname: str, trusted_list: t.Iterable[str]) -> bool: + """Check if a host matches a list of trusted names. + + :param hostname: The name to check. + :param trusted_list: A list of valid names to match. If a name + starts with a dot it will match all subdomains. + + .. versionadded:: 0.9 + """ + if not hostname: + return False + + if isinstance(trusted_list, str): + trusted_list = [trusted_list] + + def _normalize(hostname: str) -> bytes: + if ":" in hostname: + hostname = hostname.rsplit(":", 1)[0] + + return _encode_idna(hostname) + + try: + hostname_bytes = _normalize(hostname) + except UnicodeError: + return False + + for ref in trusted_list: + if ref.startswith("."): + ref = ref[1:] + suffix_match = True + else: + suffix_match = False + + try: + ref_bytes = _normalize(ref) + except UnicodeError: + return False + + if ref_bytes == hostname_bytes: + return True + + if suffix_match and hostname_bytes.endswith(b"." + ref_bytes): + return True + + return False + + +def get_host( + scheme: str, + host_header: t.Optional[str], + server: t.Optional[t.Tuple[str, t.Optional[int]]] = None, + trusted_hosts: t.Optional[t.Iterable[str]] = None, +) -> str: + """Return the host for the given parameters. + + This first checks the ``host_header``. If it's not present, then + ``server`` is used. The host will only contain the port if it is + different than the standard port for the protocol. + + Optionally, verify that the host is trusted using + :func:`host_is_trusted` and raise a + :exc:`~werkzeug.exceptions.SecurityError` if it is not. + + :param scheme: The protocol the request used, like ``"https"``. + :param host_header: The ``Host`` header value. + :param server: Address of the server. ``(host, port)``, or + ``(path, None)`` for unix sockets. + :param trusted_hosts: A list of trusted host names. + + :return: Host, with port if necessary. + :raise ~werkzeug.exceptions.SecurityError: If the host is not + trusted. + """ + host = "" + + if host_header is not None: + host = host_header + elif server is not None: + host = server[0] + + if server[1] is not None: + host = f"{host}:{server[1]}" + + if scheme in {"http", "ws"} and host.endswith(":80"): + host = host[:-3] + elif scheme in {"https", "wss"} and host.endswith(":443"): + host = host[:-4] + + if trusted_hosts is not None: + if not host_is_trusted(host, trusted_hosts): + raise SecurityError(f"Host {host!r} is not trusted.") + + return host + + +def get_current_url( + scheme: str, + host: str, + root_path: t.Optional[str] = None, + path: t.Optional[str] = None, + query_string: t.Optional[bytes] = None, +) -> str: + """Recreate the URL for a request. If an optional part isn't + provided, it and subsequent parts are not included in the URL. + + The URL is an IRI, not a URI, so it may contain Unicode characters. + Use :func:`~werkzeug.urls.iri_to_uri` to convert it to ASCII. + + :param scheme: The protocol the request used, like ``"https"``. + :param host: The host the request was made to. See :func:`get_host`. + :param root_path: Prefix that the application is mounted under. This + is prepended to ``path``. + :param path: The path part of the URL after ``root_path``. + :param query_string: The portion of the URL after the "?". + """ + url = [scheme, "://", host] + + if root_path is None: + url.append("/") + return uri_to_iri("".join(url)) + + url.append(url_quote(root_path.rstrip("/"))) + url.append("/") + + if path is None: + return uri_to_iri("".join(url)) + + url.append(url_quote(path.lstrip("/"))) + + if query_string: + url.append("?") + url.append(url_quote(query_string, safe=":&%=+$!*'(),")) + + return uri_to_iri("".join(url)) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/security.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/security.py new file mode 100644 index 000000000..e23040af9 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/security.py @@ -0,0 +1,247 @@ +import hashlib +import hmac +import os +import posixpath +import secrets +import typing as t +import warnings + +if t.TYPE_CHECKING: + pass + +SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +DEFAULT_PBKDF2_ITERATIONS = 260000 + +_os_alt_seps: t.List[str] = list( + sep for sep in [os.path.sep, os.path.altsep] if sep is not None and sep != "/" +) + + +def pbkdf2_hex( + data: t.Union[str, bytes], + salt: t.Union[str, bytes], + iterations: int = DEFAULT_PBKDF2_ITERATIONS, + keylen: t.Optional[int] = None, + hashfunc: t.Optional[t.Union[str, t.Callable]] = None, +) -> str: + """Like :func:`pbkdf2_bin`, but returns a hex-encoded string. + + :param data: the data to derive. + :param salt: the salt for the derivation. + :param iterations: the number of iterations. + :param keylen: the length of the resulting key. If not provided, + the digest size will be used. + :param hashfunc: the hash function to use. This can either be the + string name of a known hash function, or a function + from the hashlib module. Defaults to sha256. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Use :func:`hashlib.pbkdf2_hmac` + instead. + + .. versionadded:: 0.9 + """ + warnings.warn( + "'pbkdf2_hex' is deprecated and will be removed in Werkzeug" + " 2.1. Use 'hashlib.pbkdf2_hmac().hex()' instead.", + DeprecationWarning, + stacklevel=2, + ) + return pbkdf2_bin(data, salt, iterations, keylen, hashfunc).hex() + + +def pbkdf2_bin( + data: t.Union[str, bytes], + salt: t.Union[str, bytes], + iterations: int = DEFAULT_PBKDF2_ITERATIONS, + keylen: t.Optional[int] = None, + hashfunc: t.Optional[t.Union[str, t.Callable]] = None, +) -> bytes: + """Returns a binary digest for the PBKDF2 hash algorithm of `data` + with the given `salt`. It iterates `iterations` times and produces a + key of `keylen` bytes. By default, SHA-256 is used as hash function; + a different hashlib `hashfunc` can be provided. + + :param data: the data to derive. + :param salt: the salt for the derivation. + :param iterations: the number of iterations. + :param keylen: the length of the resulting key. If not provided + the digest size will be used. + :param hashfunc: the hash function to use. This can either be the + string name of a known hash function or a function + from the hashlib module. Defaults to sha256. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Use :func:`hashlib.pbkdf2_hmac` + instead. + + .. versionadded:: 0.9 + """ + warnings.warn( + "'pbkdf2_bin' is deprecated and will be removed in Werkzeug" + " 2.1. Use 'hashlib.pbkdf2_hmac()' instead.", + DeprecationWarning, + stacklevel=2, + ) + + if isinstance(data, str): + data = data.encode("utf8") + + if isinstance(salt, str): + salt = salt.encode("utf8") + + if not hashfunc: + hash_name = "sha256" + elif callable(hashfunc): + hash_name = hashfunc().name + else: + hash_name = hashfunc + + return hashlib.pbkdf2_hmac(hash_name, data, salt, iterations, keylen) + + +def safe_str_cmp(a: str, b: str) -> bool: + """This function compares strings in somewhat constant time. This + requires that the length of at least one string is known in advance. + + Returns `True` if the two strings are equal, or `False` if they are not. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Use + :func:`hmac.compare_digest` instead. + + .. versionadded:: 0.7 + """ + warnings.warn( + "'safe_str_cmp' is deprecated and will be removed in Werkzeug" + " 2.1. Use 'hmac.compare_digest' instead.", + DeprecationWarning, + stacklevel=2, + ) + + if isinstance(a, str): + a = a.encode("utf-8") # type: ignore + + if isinstance(b, str): + b = b.encode("utf-8") # type: ignore + + return hmac.compare_digest(a, b) + + +def gen_salt(length: int) -> str: + """Generate a random string of SALT_CHARS with specified ``length``.""" + if length <= 0: + raise ValueError("Salt length must be positive") + + return "".join(secrets.choice(SALT_CHARS) for _ in range(length)) + + +def _hash_internal(method: str, salt: str, password: str) -> t.Tuple[str, str]: + """Internal password hash helper. Supports plaintext without salt, + unsalted and salted passwords. In case salted passwords are used + hmac is used. + """ + if method == "plain": + return password, method + + salt = salt.encode("utf-8") + password = password.encode("utf-8") + + if method.startswith("pbkdf2:"): + if not salt: + raise ValueError("Salt is required for PBKDF2") + + args = method[7:].split(":") + + if len(args) not in (1, 2): + raise ValueError("Invalid number of arguments for PBKDF2") + + method = args.pop(0) + iterations = int(args[0] or 0) if args else DEFAULT_PBKDF2_ITERATIONS + return ( + hashlib.pbkdf2_hmac(method, password, salt, iterations).hex(), + f"pbkdf2:{method}:{iterations}", + ) + + if salt: + return hmac.new(salt, password, method).hexdigest(), method + + return hashlib.new(method, password).hexdigest(), method + + +def generate_password_hash( + password: str, method: str = "pbkdf2:sha256", salt_length: int = 16 +) -> str: + """Hash a password with the given method and salt with a string of + the given length. The format of the string returned includes the method + that was used so that :func:`check_password_hash` can check the hash. + + The format for the hashed string looks like this:: + + method$salt$hash + + This method can **not** generate unsalted passwords but it is possible + to set param method='plain' in order to enforce plaintext passwords. + If a salt is used, hmac is used internally to salt the password. + + If PBKDF2 is wanted it can be enabled by setting the method to + ``pbkdf2:method:iterations`` where iterations is optional:: + + pbkdf2:sha256:80000$salt$hash + pbkdf2:sha256$salt$hash + + :param password: the password to hash. + :param method: the hash method to use (one that hashlib supports). Can + optionally be in the format ``pbkdf2:method:iterations`` + to enable PBKDF2. + :param salt_length: the length of the salt in letters. + """ + salt = gen_salt(salt_length) if method != "plain" else "" + h, actual_method = _hash_internal(method, salt, password) + return f"{actual_method}${salt}${h}" + + +def check_password_hash(pwhash: str, password: str) -> bool: + """Check a password against a given salted and hashed password value. + In order to support unsalted legacy passwords this method supports + plain text passwords, md5 and sha1 hashes (both salted and unsalted). + + Returns `True` if the password matched, `False` otherwise. + + :param pwhash: a hashed string like returned by + :func:`generate_password_hash`. + :param password: the plaintext password to compare against the hash. + """ + if pwhash.count("$") < 2: + return False + + method, salt, hashval = pwhash.split("$", 2) + return hmac.compare_digest(_hash_internal(method, salt, password)[0], hashval) + + +def safe_join(directory: str, *pathnames: str) -> t.Optional[str]: + """Safely join zero or more untrusted path components to a base + directory to avoid escaping the base directory. + + :param directory: The trusted base directory. + :param pathnames: The untrusted path components relative to the + base directory. + :return: A safe path, otherwise ``None``. + """ + parts = [directory] + + for filename in pathnames: + if filename != "": + filename = posixpath.normpath(filename) + + if ( + any(sep in filename for sep in _os_alt_seps) + or os.path.isabs(filename) + or filename == ".." + or filename.startswith("../") + ): + return None + + parts.append(filename) + + return posixpath.join(*parts) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/serving.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/serving.py new file mode 100644 index 000000000..197b6fb9a --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/serving.py @@ -0,0 +1,1081 @@ +"""A WSGI and HTTP server for use **during development only**. This +server is convenient to use, but is not designed to be particularly +stable, secure, or efficient. Use a dedicate WSGI server and HTTP +server when deploying to production. + +It provides features like interactive debugging and code reloading. Use +``run_simple`` to start the server. Put this in a ``run.py`` script: + +.. code-block:: python + + from myapp import create_app + from werkzeug import run_simple +""" +import io +import os +import platform +import signal +import socket +import socketserver +import sys +import typing as t +import warnings +from datetime import datetime as dt +from datetime import timedelta +from datetime import timezone +from http.server import BaseHTTPRequestHandler +from http.server import HTTPServer + +from ._internal import _log +from ._internal import _wsgi_encoding_dance +from .exceptions import InternalServerError +from .urls import uri_to_iri +from .urls import url_parse +from .urls import url_unquote + +try: + import ssl +except ImportError: + + class _SslDummy: + def __getattr__(self, name: str) -> t.Any: + raise RuntimeError("SSL support unavailable") # noqa: B904 + + ssl = _SslDummy() # type: ignore + +_log_add_style = True + +if os.name == "nt": + try: + __import__("colorama") + except ImportError: + _log_add_style = False + +can_fork = hasattr(os, "fork") + +if can_fork: + ForkingMixIn = socketserver.ForkingMixIn +else: + + class ForkingMixIn: # type: ignore + pass + + +try: + af_unix = socket.AF_UNIX +except AttributeError: + af_unix = None # type: ignore + +LISTEN_QUEUE = 128 +can_open_by_fd = not platform.system() == "Windows" and hasattr(socket, "fromfd") + +_TSSLContextArg = t.Optional[ + t.Union["ssl.SSLContext", t.Tuple[str, t.Optional[str]], "te.Literal['adhoc']"] +] + +if t.TYPE_CHECKING: + import typing_extensions as te # noqa: F401 + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + from cryptography.hazmat.primitives.asymmetric.rsa import ( + RSAPrivateKeyWithSerialization, + ) + from cryptography.x509 import Certificate + + +class DechunkedInput(io.RawIOBase): + """An input stream that handles Transfer-Encoding 'chunked'""" + + def __init__(self, rfile: t.IO[bytes]) -> None: + self._rfile = rfile + self._done = False + self._len = 0 + + def readable(self) -> bool: + return True + + def read_chunk_len(self) -> int: + try: + line = self._rfile.readline().decode("latin1") + _len = int(line.strip(), 16) + except ValueError as e: + raise OSError("Invalid chunk header") from e + if _len < 0: + raise OSError("Negative chunk length not allowed") + return _len + + def readinto(self, buf: bytearray) -> int: # type: ignore + read = 0 + while not self._done and read < len(buf): + if self._len == 0: + # This is the first chunk or we fully consumed the previous + # one. Read the next length of the next chunk + self._len = self.read_chunk_len() + + if self._len == 0: + # Found the final chunk of size 0. The stream is now exhausted, + # but there is still a final newline that should be consumed + self._done = True + + if self._len > 0: + # There is data (left) in this chunk, so append it to the + # buffer. If this operation fully consumes the chunk, this will + # reset self._len to 0. + n = min(len(buf), self._len) + + # If (read + chunk size) becomes more than len(buf), buf will + # grow beyond the original size and read more data than + # required. So only read as much data as can fit in buf. + if read + n > len(buf): + buf[read:] = self._rfile.read(len(buf) - read) + self._len -= len(buf) - read + read = len(buf) + else: + buf[read : read + n] = self._rfile.read(n) + self._len -= n + read += n + + if self._len == 0: + # Skip the terminating newline of a chunk that has been fully + # consumed. This also applies to the 0-sized final chunk + terminator = self._rfile.readline() + if terminator not in (b"\n", b"\r\n", b"\r"): + raise OSError("Missing chunk terminating newline") + + return read + + +class WSGIRequestHandler(BaseHTTPRequestHandler): + """A request handler that implements WSGI dispatching.""" + + server: "BaseWSGIServer" + + @property + def server_version(self) -> str: # type: ignore + from . import __version__ + + return f"Werkzeug/{__version__}" + + def make_environ(self) -> "WSGIEnvironment": + request_url = url_parse(self.path) + + def shutdown_server() -> None: + warnings.warn( + "The 'environ['werkzeug.server.shutdown']' function is" + " deprecated and will be removed in Werkzeug 2.1.", + stacklevel=2, + ) + self.server.shutdown_signal = True + + url_scheme = "http" if self.server.ssl_context is None else "https" + + if not self.client_address: + self.client_address = ("", 0) + elif isinstance(self.client_address, str): + self.client_address = (self.client_address, 0) + + # If there was no scheme but the path started with two slashes, + # the first segment may have been incorrectly parsed as the + # netloc, prepend it to the path again. + if not request_url.scheme and request_url.netloc: + path_info = f"/{request_url.netloc}{request_url.path}" + else: + path_info = request_url.path + + path_info = url_unquote(path_info) + + environ: "WSGIEnvironment" = { + "wsgi.version": (1, 0), + "wsgi.url_scheme": url_scheme, + "wsgi.input": self.rfile, + "wsgi.errors": sys.stderr, + "wsgi.multithread": self.server.multithread, + "wsgi.multiprocess": self.server.multiprocess, + "wsgi.run_once": False, + "werkzeug.server.shutdown": shutdown_server, + "werkzeug.socket": self.connection, + "SERVER_SOFTWARE": self.server_version, + "REQUEST_METHOD": self.command, + "SCRIPT_NAME": "", + "PATH_INFO": _wsgi_encoding_dance(path_info), + "QUERY_STRING": _wsgi_encoding_dance(request_url.query), + # Non-standard, added by mod_wsgi, uWSGI + "REQUEST_URI": _wsgi_encoding_dance(self.path), + # Non-standard, added by gunicorn + "RAW_URI": _wsgi_encoding_dance(self.path), + "REMOTE_ADDR": self.address_string(), + "REMOTE_PORT": self.port_integer(), + "SERVER_NAME": self.server.server_address[0], + "SERVER_PORT": str(self.server.server_address[1]), + "SERVER_PROTOCOL": self.request_version, + } + + for key, value in self.headers.items(): + key = key.upper().replace("-", "_") + value = value.replace("\r\n", "") + if key not in ("CONTENT_TYPE", "CONTENT_LENGTH"): + key = f"HTTP_{key}" + if key in environ: + value = f"{environ[key]},{value}" + environ[key] = value + + if environ.get("HTTP_TRANSFER_ENCODING", "").strip().lower() == "chunked": + environ["wsgi.input_terminated"] = True + environ["wsgi.input"] = DechunkedInput(environ["wsgi.input"]) + + # Per RFC 2616, if the URL is absolute, use that as the host. + # We're using "has a scheme" to indicate an absolute URL. + if request_url.scheme and request_url.netloc: + environ["HTTP_HOST"] = request_url.netloc + + try: + # binary_form=False gives nicer information, but wouldn't be compatible with + # what Nginx or Apache could return. + peer_cert = self.connection.getpeercert(binary_form=True) + if peer_cert is not None: + # Nginx and Apache use PEM format. + environ["SSL_CLIENT_CERT"] = ssl.DER_cert_to_PEM_cert(peer_cert) + except ValueError: + # SSL handshake hasn't finished. + self.server.log("error", "Cannot fetch SSL peer certificate info") + except AttributeError: + # Not using TLS, the socket will not have getpeercert(). + pass + + return environ + + def run_wsgi(self) -> None: + if self.headers.get("Expect", "").lower().strip() == "100-continue": + self.wfile.write(b"HTTP/1.1 100 Continue\r\n\r\n") + + self.environ = environ = self.make_environ() + status_set: t.Optional[str] = None + headers_set: t.Optional[t.List[t.Tuple[str, str]]] = None + status_sent: t.Optional[str] = None + headers_sent: t.Optional[t.List[t.Tuple[str, str]]] = None + + def write(data: bytes) -> None: + nonlocal status_sent, headers_sent + assert status_set is not None, "write() before start_response" + assert headers_set is not None, "write() before start_response" + if status_sent is None: + status_sent = status_set + headers_sent = headers_set + try: + code_str, msg = status_sent.split(None, 1) + except ValueError: + code_str, msg = status_sent, "" + code = int(code_str) + self.send_response(code, msg) + header_keys = set() + for key, value in headers_sent: + self.send_header(key, value) + key = key.lower() + header_keys.add(key) + if not ( + "content-length" in header_keys + or environ["REQUEST_METHOD"] == "HEAD" + or code < 200 + or code in (204, 304) + ): + self.close_connection = True + self.send_header("Connection", "close") + if "server" not in header_keys: + self.send_header("Server", self.version_string()) + if "date" not in header_keys: + self.send_header("Date", self.date_time_string()) + self.end_headers() + + assert isinstance(data, bytes), "applications must write bytes" + self.wfile.write(data) + self.wfile.flush() + + def start_response(status, headers, exc_info=None): # type: ignore + nonlocal status_set, headers_set + if exc_info: + try: + if headers_sent: + raise exc_info[1].with_traceback(exc_info[2]) + finally: + exc_info = None + elif headers_set: + raise AssertionError("Headers already set") + status_set = status + headers_set = headers + return write + + def execute(app: "WSGIApplication") -> None: + application_iter = app(environ, start_response) + try: + for data in application_iter: + write(data) + if not headers_sent: + write(b"") + finally: + if hasattr(application_iter, "close"): + application_iter.close() # type: ignore + + try: + execute(self.server.app) + except (ConnectionError, socket.timeout) as e: + self.connection_dropped(e, environ) + except Exception: + if self.server.passthrough_errors: + raise + from .debug.tbtools import get_current_traceback + + traceback = get_current_traceback(ignore_system_exceptions=True) + try: + # if we haven't yet sent the headers but they are set + # we roll back to be able to set them again. + if status_sent is None: + status_set = None + headers_set = None + execute(InternalServerError()) + except Exception: + pass + self.server.log("error", "Error on request:\n%s", traceback.plaintext) + + def handle(self) -> None: + """Handles a request ignoring dropped connections.""" + try: + BaseHTTPRequestHandler.handle(self) + except (ConnectionError, socket.timeout) as e: + self.connection_dropped(e) + except Exception as e: + if self.server.ssl_context is not None and is_ssl_error(e): + self.log_error("SSL error occurred: %s", e) + else: + raise + if self.server.shutdown_signal: + self.initiate_shutdown() + + def initiate_shutdown(self) -> None: + if is_running_from_reloader(): + # Windows does not provide SIGKILL, go with SIGTERM then. + sig = getattr(signal, "SIGKILL", signal.SIGTERM) + os.kill(os.getpid(), sig) + + self.server._BaseServer__shutdown_request = True # type: ignore + + def connection_dropped( + self, error: BaseException, environ: t.Optional["WSGIEnvironment"] = None + ) -> None: + """Called if the connection was closed by the client. By default + nothing happens. + """ + + def handle_one_request(self) -> None: + """Handle a single HTTP request.""" + self.raw_requestline = self.rfile.readline() + if not self.raw_requestline: + self.close_connection = True + elif self.parse_request(): + self.run_wsgi() + + def send_response(self, code: int, message: t.Optional[str] = None) -> None: + """Send the response header and log the response code.""" + self.log_request(code) + if message is None: + message = self.responses[code][0] if code in self.responses else "" + if self.request_version != "HTTP/0.9": + hdr = f"{self.protocol_version} {code} {message}\r\n" + self.wfile.write(hdr.encode("ascii")) + + def version_string(self) -> str: + return super().version_string().strip() + + def address_string(self) -> str: + if getattr(self, "environ", None): + return self.environ["REMOTE_ADDR"] # type: ignore + + if not self.client_address: + return "" + + return self.client_address[0] + + def port_integer(self) -> int: + return self.client_address[1] + + def log_request( + self, code: t.Union[int, str] = "-", size: t.Union[int, str] = "-" + ) -> None: + try: + path = uri_to_iri(self.path) + msg = f"{self.command} {path} {self.request_version}" + except AttributeError: + # path isn't set if the requestline was bad + msg = self.requestline + + code = str(code) + + if _log_add_style: + if code[0] == "1": # 1xx - Informational + msg = _ansi_style(msg, "bold") + elif code == "200": # 2xx - Success + pass + elif code == "304": # 304 - Resource Not Modified + msg = _ansi_style(msg, "cyan") + elif code[0] == "3": # 3xx - Redirection + msg = _ansi_style(msg, "green") + elif code == "404": # 404 - Resource Not Found + msg = _ansi_style(msg, "yellow") + elif code[0] == "4": # 4xx - Client Error + msg = _ansi_style(msg, "bold", "red") + else: # 5xx, or any other response + msg = _ansi_style(msg, "bold", "magenta") + + self.log("info", '"%s" %s %s', msg, code, size) + + def log_error(self, format: str, *args: t.Any) -> None: + self.log("error", format, *args) + + def log_message(self, format: str, *args: t.Any) -> None: + self.log("info", format, *args) + + def log(self, type: str, message: str, *args: t.Any) -> None: + _log( + type, + f"{self.address_string()} - - [{self.log_date_time_string()}] {message}\n", + *args, + ) + + +def _ansi_style(value: str, *styles: str) -> str: + codes = { + "bold": 1, + "red": 31, + "green": 32, + "yellow": 33, + "magenta": 35, + "cyan": 36, + } + + for style in styles: + value = f"\x1b[{codes[style]}m{value}" + + return f"{value}\x1b[0m" + + +def generate_adhoc_ssl_pair( + cn: t.Optional[str] = None, +) -> t.Tuple["Certificate", "RSAPrivateKeyWithSerialization"]: + try: + from cryptography import x509 + from cryptography.x509.oid import NameOID + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import rsa + except ImportError: + raise TypeError( + "Using ad-hoc certificates requires the cryptography library." + ) from None + + backend = default_backend() + pkey = rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=backend + ) + + # pretty damn sure that this is not actually accepted by anyone + if cn is None: + cn = "*" + + subject = x509.Name( + [ + x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Dummy Certificate"), + x509.NameAttribute(NameOID.COMMON_NAME, cn), + ] + ) + + backend = default_backend() + cert = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(subject) + .public_key(pkey.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(dt.now(timezone.utc)) + .not_valid_after(dt.now(timezone.utc) + timedelta(days=365)) + .add_extension(x509.ExtendedKeyUsage([x509.OID_SERVER_AUTH]), critical=False) + .add_extension(x509.SubjectAlternativeName([x509.DNSName(cn)]), critical=False) + .sign(pkey, hashes.SHA256(), backend) + ) + return cert, pkey + + +def make_ssl_devcert( + base_path: str, host: t.Optional[str] = None, cn: t.Optional[str] = None +) -> t.Tuple[str, str]: + """Creates an SSL key for development. This should be used instead of + the ``'adhoc'`` key which generates a new cert on each server start. + It accepts a path for where it should store the key and cert and + either a host or CN. If a host is given it will use the CN + ``*.host/CN=host``. + + For more information see :func:`run_simple`. + + .. versionadded:: 0.9 + + :param base_path: the path to the certificate and key. The extension + ``.crt`` is added for the certificate, ``.key`` is + added for the key. + :param host: the name of the host. This can be used as an alternative + for the `cn`. + :param cn: the `CN` to use. + """ + + if host is not None: + cn = f"*.{host}/CN={host}" + cert, pkey = generate_adhoc_ssl_pair(cn=cn) + + from cryptography.hazmat.primitives import serialization + + cert_file = f"{base_path}.crt" + pkey_file = f"{base_path}.key" + + with open(cert_file, "wb") as f: + f.write(cert.public_bytes(serialization.Encoding.PEM)) + with open(pkey_file, "wb") as f: + f.write( + pkey.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + ) + + return cert_file, pkey_file + + +def generate_adhoc_ssl_context() -> "ssl.SSLContext": + """Generates an adhoc SSL context for the development server.""" + import tempfile + import atexit + + cert, pkey = generate_adhoc_ssl_pair() + + from cryptography.hazmat.primitives import serialization + + cert_handle, cert_file = tempfile.mkstemp() + pkey_handle, pkey_file = tempfile.mkstemp() + atexit.register(os.remove, pkey_file) + atexit.register(os.remove, cert_file) + + os.write(cert_handle, cert.public_bytes(serialization.Encoding.PEM)) + os.write( + pkey_handle, + pkey.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ), + ) + + os.close(cert_handle) + os.close(pkey_handle) + ctx = load_ssl_context(cert_file, pkey_file) + return ctx + + +def load_ssl_context( + cert_file: str, pkey_file: t.Optional[str] = None, protocol: t.Optional[int] = None +) -> "ssl.SSLContext": + """Loads SSL context from cert/private key files and optional protocol. + Many parameters are directly taken from the API of + :py:class:`ssl.SSLContext`. + + :param cert_file: Path of the certificate to use. + :param pkey_file: Path of the private key to use. If not given, the key + will be obtained from the certificate file. + :param protocol: A ``PROTOCOL`` constant from the :mod:`ssl` module. + Defaults to :data:`ssl.PROTOCOL_TLS_SERVER`. + """ + if protocol is None: + protocol = ssl.PROTOCOL_TLS_SERVER + + ctx = ssl.SSLContext(protocol) + ctx.load_cert_chain(cert_file, pkey_file) + return ctx + + +def is_ssl_error(error: t.Optional[Exception] = None) -> bool: + """Checks if the given error (or the current one) is an SSL error.""" + if error is None: + error = t.cast(Exception, sys.exc_info()[1]) + return isinstance(error, ssl.SSLError) + + +def select_address_family(host: str, port: int) -> socket.AddressFamily: + """Return ``AF_INET4``, ``AF_INET6``, or ``AF_UNIX`` depending on + the host and port.""" + if host.startswith("unix://"): + return socket.AF_UNIX + elif ":" in host and hasattr(socket, "AF_INET6"): + return socket.AF_INET6 + return socket.AF_INET + + +def get_sockaddr( + host: str, port: int, family: socket.AddressFamily +) -> t.Union[t.Tuple[str, int], str]: + """Return a fully qualified socket address that can be passed to + :func:`socket.bind`.""" + if family == af_unix: + return host.split("://", 1)[1] + try: + res = socket.getaddrinfo( + host, port, family, socket.SOCK_STREAM, socket.IPPROTO_TCP + ) + except socket.gaierror: + return host, port + return res[0][4] # type: ignore + + +def get_interface_ip(family: socket.AddressFamily) -> str: + """Get the IP address of an external interface. Used when binding to + 0.0.0.0 or ::1 to show a more useful URL. + + :meta private: + """ + # arbitrary private address + host = "fd31:f903:5ab5:1::1" if family == socket.AF_INET6 else "10.253.155.219" + + with socket.socket(family, socket.SOCK_DGRAM) as s: + try: + s.connect((host, 58162)) + except OSError: + return "::1" if family == socket.AF_INET6 else "127.0.0.1" + + return s.getsockname()[0] # type: ignore + + +class BaseWSGIServer(HTTPServer): + + """Simple single-threaded, single-process WSGI server.""" + + multithread = False + multiprocess = False + request_queue_size = LISTEN_QUEUE + + def __init__( + self, + host: str, + port: int, + app: "WSGIApplication", + handler: t.Optional[t.Type[WSGIRequestHandler]] = None, + passthrough_errors: bool = False, + ssl_context: t.Optional[_TSSLContextArg] = None, + fd: t.Optional[int] = None, + ) -> None: + if handler is None: + handler = WSGIRequestHandler + + self.address_family = select_address_family(host, port) + + if fd is not None: + real_sock = socket.fromfd(fd, self.address_family, socket.SOCK_STREAM) + port = 0 + + server_address = get_sockaddr(host, int(port), self.address_family) + + # remove socket file if it already exists + if self.address_family == af_unix: + server_address = t.cast(str, server_address) + + if os.path.exists(server_address): + os.unlink(server_address) + + super().__init__(server_address, handler) # type: ignore + + self.app = app + self.passthrough_errors = passthrough_errors + self.shutdown_signal = False + self.host = host + self.port = self.socket.getsockname()[1] + + # Patch in the original socket. + if fd is not None: + self.socket.close() + self.socket = real_sock + self.server_address = self.socket.getsockname() + + if ssl_context is not None: + if isinstance(ssl_context, tuple): + ssl_context = load_ssl_context(*ssl_context) + if ssl_context == "adhoc": + ssl_context = generate_adhoc_ssl_context() + + self.socket = ssl_context.wrap_socket(self.socket, server_side=True) + self.ssl_context: t.Optional["ssl.SSLContext"] = ssl_context + else: + self.ssl_context = None + + def log(self, type: str, message: str, *args: t.Any) -> None: + _log(type, message, *args) + + def serve_forever(self, poll_interval: float = 0.5) -> None: + self.shutdown_signal = False + try: + super().serve_forever(poll_interval=poll_interval) + except KeyboardInterrupt: + pass + finally: + self.server_close() + + def handle_error(self, request: t.Any, client_address: t.Tuple[str, int]) -> None: + if self.passthrough_errors: + raise + + return super().handle_error(request, client_address) + + +class ThreadedWSGIServer(socketserver.ThreadingMixIn, BaseWSGIServer): + + """A WSGI server that does threading.""" + + multithread = True + daemon_threads = True + + +class ForkingWSGIServer(ForkingMixIn, BaseWSGIServer): + + """A WSGI server that does forking.""" + + multiprocess = True + + def __init__( + self, + host: str, + port: int, + app: "WSGIApplication", + processes: int = 40, + handler: t.Optional[t.Type[WSGIRequestHandler]] = None, + passthrough_errors: bool = False, + ssl_context: t.Optional[_TSSLContextArg] = None, + fd: t.Optional[int] = None, + ) -> None: + if not can_fork: + raise ValueError("Your platform does not support forking.") + BaseWSGIServer.__init__( + self, host, port, app, handler, passthrough_errors, ssl_context, fd + ) + self.max_children = processes + + +def make_server( + host: str, + port: int, + app: "WSGIApplication", + threaded: bool = False, + processes: int = 1, + request_handler: t.Optional[t.Type[WSGIRequestHandler]] = None, + passthrough_errors: bool = False, + ssl_context: t.Optional[_TSSLContextArg] = None, + fd: t.Optional[int] = None, +) -> BaseWSGIServer: + """Create a new server instance that is either threaded, or forks + or just processes one request after another. + """ + if threaded and processes > 1: + raise ValueError("cannot have a multithreaded and multi process server.") + elif threaded: + return ThreadedWSGIServer( + host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd + ) + elif processes > 1: + return ForkingWSGIServer( + host, + port, + app, + processes, + request_handler, + passthrough_errors, + ssl_context, + fd=fd, + ) + else: + return BaseWSGIServer( + host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd + ) + + +def is_running_from_reloader() -> bool: + """Checks if the application is running from within the Werkzeug + reloader subprocess. + + .. versionadded:: 0.10 + """ + return os.environ.get("WERKZEUG_RUN_MAIN") == "true" + + +def run_simple( + hostname: str, + port: int, + application: "WSGIApplication", + use_reloader: bool = False, + use_debugger: bool = False, + use_evalex: bool = True, + extra_files: t.Optional[t.Iterable[str]] = None, + exclude_patterns: t.Optional[t.Iterable[str]] = None, + reloader_interval: int = 1, + reloader_type: str = "auto", + threaded: bool = False, + processes: int = 1, + request_handler: t.Optional[t.Type[WSGIRequestHandler]] = None, + static_files: t.Optional[t.Dict[str, t.Union[str, t.Tuple[str, str]]]] = None, + passthrough_errors: bool = False, + ssl_context: t.Optional[_TSSLContextArg] = None, +) -> None: + """Start a WSGI application. Optional features include a reloader, + multithreading and fork support. + + This function has a command-line interface too:: + + python -m werkzeug.serving --help + + .. versionchanged:: 2.0 + Added ``exclude_patterns`` parameter. + + .. versionadded:: 0.5 + `static_files` was added to simplify serving of static files as well + as `passthrough_errors`. + + .. versionadded:: 0.6 + support for SSL was added. + + .. versionadded:: 0.8 + Added support for automatically loading a SSL context from certificate + file and private key. + + .. versionadded:: 0.9 + Added command-line interface. + + .. versionadded:: 0.10 + Improved the reloader and added support for changing the backend + through the `reloader_type` parameter. See :ref:`reloader` + for more information. + + .. versionchanged:: 0.15 + Bind to a Unix socket by passing a path that starts with + ``unix://`` as the ``hostname``. + + :param hostname: The host to bind to, for example ``'localhost'``. + If the value is a path that starts with ``unix://`` it will bind + to a Unix socket instead of a TCP socket.. + :param port: The port for the server. eg: ``8080`` + :param application: the WSGI application to execute + :param use_reloader: should the server automatically restart the python + process if modules were changed? + :param use_debugger: should the werkzeug debugging system be used? + :param use_evalex: should the exception evaluation feature be enabled? + :param extra_files: a list of files the reloader should watch + additionally to the modules. For example configuration + files. + :param exclude_patterns: List of :mod:`fnmatch` patterns to ignore + when running the reloader. For example, ignore cache files that + shouldn't reload when updated. + :param reloader_interval: the interval for the reloader in seconds. + :param reloader_type: the type of reloader to use. The default is + auto detection. Valid values are ``'stat'`` and + ``'watchdog'``. See :ref:`reloader` for more + information. + :param threaded: should the process handle each request in a separate + thread? + :param processes: if greater than 1 then handle each request in a new process + up to this maximum number of concurrent processes. + :param request_handler: optional parameter that can be used to replace + the default one. You can use this to replace it + with a different + :class:`~BaseHTTPServer.BaseHTTPRequestHandler` + subclass. + :param static_files: a list or dict of paths for static files. This works + exactly like :class:`SharedDataMiddleware`, it's actually + just wrapping the application in that middleware before + serving. + :param passthrough_errors: set this to `True` to disable the error catching. + This means that the server will die on errors but + it can be useful to hook debuggers in (pdb etc.) + :param ssl_context: an SSL context for the connection. Either an + :class:`ssl.SSLContext`, a tuple in the form + ``(cert_file, pkey_file)``, the string ``'adhoc'`` if + the server should automatically create one, or ``None`` + to disable SSL (which is the default). + """ + if not isinstance(port, int): + raise TypeError("port must be an integer") + if use_debugger: + from .debug import DebuggedApplication + + application = DebuggedApplication(application, use_evalex) + if static_files: + from .middleware.shared_data import SharedDataMiddleware + + application = SharedDataMiddleware(application, static_files) + + def log_startup(sock: socket.socket) -> None: + all_addresses_message = ( + " * Running on all addresses.\n" + " WARNING: This is a development server. Do not use it in" + " a production deployment." + ) + + if sock.family == af_unix: + _log("info", " * Running on %s (Press CTRL+C to quit)", hostname) + else: + if hostname == "0.0.0.0": + _log("warning", all_addresses_message) + display_hostname = get_interface_ip(socket.AF_INET) + elif hostname == "::": + _log("warning", all_addresses_message) + display_hostname = get_interface_ip(socket.AF_INET6) + else: + display_hostname = hostname + + if ":" in display_hostname: + display_hostname = f"[{display_hostname}]" + + _log( + "info", + " * Running on %s://%s:%d/ (Press CTRL+C to quit)", + "http" if ssl_context is None else "https", + display_hostname, + sock.getsockname()[1], + ) + + def inner() -> None: + try: + fd: t.Optional[int] = int(os.environ["WERKZEUG_SERVER_FD"]) + except (LookupError, ValueError): + fd = None + srv = make_server( + hostname, + port, + application, + threaded, + processes, + request_handler, + passthrough_errors, + ssl_context, + fd=fd, + ) + if fd is None: + log_startup(srv.socket) + srv.serve_forever() + + if use_reloader: + # If we're not running already in the subprocess that is the + # reloader we want to open up a socket early to make sure the + # port is actually available. + if not is_running_from_reloader(): + if port == 0 and not can_open_by_fd: + raise ValueError( + "Cannot bind to a random port with enabled " + "reloader if the Python interpreter does " + "not support socket opening by fd." + ) + + # Create and destroy a socket so that any exceptions are + # raised before we spawn a separate Python interpreter and + # lose this ability. + address_family = select_address_family(hostname, port) + server_address = get_sockaddr(hostname, port, address_family) + s = socket.socket(address_family, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(server_address) + s.set_inheritable(True) + + # If we can open the socket by file descriptor, then we can just + # reuse this one and our socket will survive the restarts. + if can_open_by_fd: + os.environ["WERKZEUG_SERVER_FD"] = str(s.fileno()) + s.listen(LISTEN_QUEUE) + log_startup(s) + else: + s.close() + if address_family == af_unix: + server_address = t.cast(str, server_address) + _log("info", "Unlinking %s", server_address) + os.unlink(server_address) + + from ._reloader import run_with_reloader as _rwr + + _rwr( + inner, + extra_files=extra_files, + exclude_patterns=exclude_patterns, + interval=reloader_interval, + reloader_type=reloader_type, + ) + else: + inner() + + +def run_with_reloader(*args: t.Any, **kwargs: t.Any) -> None: + """Run a process with the reloader. This is not a public API, do + not use this function. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. + """ + from ._reloader import run_with_reloader as _rwr + + warnings.warn( + ( + "'run_with_reloader' is a private API, it will no longer be" + " accessible in Werkzeug 2.1. Use 'run_simple' instead." + ), + DeprecationWarning, + stacklevel=2, + ) + _rwr(*args, **kwargs) + + +def main() -> None: + """A simple command-line interface for :py:func:`run_simple`.""" + import argparse + from .utils import import_string + + _log("warning", "This CLI is deprecated and will be removed in version 2.1.") + + parser = argparse.ArgumentParser( + description="Run the given WSGI application with the development server.", + allow_abbrev=False, + ) + parser.add_argument( + "-b", + "--bind", + dest="address", + help="The hostname:port the app should listen on.", + ) + parser.add_argument( + "-d", + "--debug", + action="store_true", + help="Show the interactive debugger for unhandled exceptions.", + ) + parser.add_argument( + "-r", + "--reload", + action="store_true", + help="Reload the process if modules change.", + ) + parser.add_argument( + "application", help="Application to import and serve, in the form module:app." + ) + args = parser.parse_args() + hostname, port = None, None + + if args.address: + hostname, _, port = args.address.partition(":") + + run_simple( + hostname=hostname or "127.0.0.1", + port=int(port or 5000), + application=import_string(args.application), + use_reloader=args.reload, + use_debugger=args.debug, + ) + + +if __name__ == "__main__": + main() diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/test.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/test.py new file mode 100644 index 000000000..09cb7e89c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/test.py @@ -0,0 +1,1326 @@ +import mimetypes +import sys +import typing as t +import warnings +from collections import defaultdict +from datetime import datetime +from datetime import timedelta +from http.cookiejar import CookieJar +from io import BytesIO +from itertools import chain +from random import random +from tempfile import TemporaryFile +from time import time +from urllib.request import Request as _UrllibRequest + +from ._internal import _get_environ +from ._internal import _make_encode_wrapper +from ._internal import _wsgi_decoding_dance +from ._internal import _wsgi_encoding_dance +from .datastructures import Authorization +from .datastructures import CallbackDict +from .datastructures import CombinedMultiDict +from .datastructures import EnvironHeaders +from .datastructures import FileMultiDict +from .datastructures import Headers +from .datastructures import MultiDict +from .http import dump_cookie +from .http import dump_options_header +from .http import parse_options_header +from .sansio.multipart import Data +from .sansio.multipart import Epilogue +from .sansio.multipart import Field +from .sansio.multipart import File +from .sansio.multipart import MultipartEncoder +from .sansio.multipart import Preamble +from .urls import iri_to_uri +from .urls import url_encode +from .urls import url_fix +from .urls import url_parse +from .urls import url_unparse +from .urls import url_unquote +from .utils import get_content_type +from .wrappers.request import Request +from .wrappers.response import Response +from .wsgi import ClosingIterator +from .wsgi import get_current_url + +if t.TYPE_CHECKING: + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + + +def stream_encode_multipart( + data: t.Mapping[str, t.Any], + use_tempfile: bool = True, + threshold: int = 1024 * 500, + boundary: t.Optional[str] = None, + charset: str = "utf-8", +) -> t.Tuple[t.IO[bytes], int, str]: + """Encode a dict of values (either strings or file descriptors or + :class:`FileStorage` objects.) into a multipart encoded string stored + in a file descriptor. + """ + if boundary is None: + boundary = f"---------------WerkzeugFormPart_{time()}{random()}" + + stream: t.IO[bytes] = BytesIO() + total_length = 0 + on_disk = False + + if use_tempfile: + + def write_binary(s: bytes) -> int: + nonlocal stream, total_length, on_disk + + if on_disk: + return stream.write(s) + else: + length = len(s) + + if length + total_length <= threshold: + stream.write(s) + else: + new_stream = t.cast(t.IO[bytes], TemporaryFile("wb+")) + new_stream.write(stream.getvalue()) # type: ignore + new_stream.write(s) + stream = new_stream + on_disk = True + + total_length += length + return length + + else: + write_binary = stream.write + + encoder = MultipartEncoder(boundary.encode()) + write_binary(encoder.send_event(Preamble(data=b""))) + for key, value in _iter_data(data): + reader = getattr(value, "read", None) + if reader is not None: + filename = getattr(value, "filename", getattr(value, "name", None)) + content_type = getattr(value, "content_type", None) + if content_type is None: + content_type = ( + filename + and mimetypes.guess_type(filename)[0] + or "application/octet-stream" + ) + headers = Headers([("Content-Type", content_type)]) + if filename is None: + write_binary(encoder.send_event(Field(name=key, headers=headers))) + else: + write_binary( + encoder.send_event( + File(name=key, filename=filename, headers=headers) + ) + ) + while True: + chunk = reader(16384) + + if not chunk: + break + + write_binary(encoder.send_event(Data(data=chunk, more_data=True))) + else: + if not isinstance(value, str): + value = str(value) + write_binary(encoder.send_event(Field(name=key, headers=Headers()))) + write_binary( + encoder.send_event(Data(data=value.encode(charset), more_data=False)) + ) + + write_binary(encoder.send_event(Epilogue(data=b""))) + + length = stream.tell() + stream.seek(0) + return stream, length, boundary + + +def encode_multipart( + values: t.Mapping[str, t.Any], + boundary: t.Optional[str] = None, + charset: str = "utf-8", +) -> t.Tuple[str, bytes]: + """Like `stream_encode_multipart` but returns a tuple in the form + (``boundary``, ``data``) where data is bytes. + """ + stream, length, boundary = stream_encode_multipart( + values, use_tempfile=False, boundary=boundary, charset=charset + ) + return boundary, stream.read() + + +class _TestCookieHeaders: + """A headers adapter for cookielib""" + + def __init__(self, headers: t.Union[Headers, t.List[t.Tuple[str, str]]]) -> None: + self.headers = headers + + def getheaders(self, name: str) -> t.Iterable[str]: + headers = [] + name = name.lower() + for k, v in self.headers: + if k.lower() == name: + headers.append(v) + return headers + + def get_all( + self, name: str, default: t.Optional[t.Iterable[str]] = None + ) -> t.Iterable[str]: + headers = self.getheaders(name) + + if not headers: + return default # type: ignore + + return headers + + +class _TestCookieResponse: + """Something that looks like a httplib.HTTPResponse, but is actually just an + adapter for our test responses to make them available for cookielib. + """ + + def __init__(self, headers: t.Union[Headers, t.List[t.Tuple[str, str]]]) -> None: + self.headers = _TestCookieHeaders(headers) + + def info(self) -> _TestCookieHeaders: + return self.headers + + +class _TestCookieJar(CookieJar): + """A cookielib.CookieJar modified to inject and read cookie headers from + and to wsgi environments, and wsgi application responses. + """ + + def inject_wsgi(self, environ: "WSGIEnvironment") -> None: + """Inject the cookies as client headers into the server's wsgi + environment. + """ + cvals = [f"{c.name}={c.value}" for c in self] + + if cvals: + environ["HTTP_COOKIE"] = "; ".join(cvals) + else: + environ.pop("HTTP_COOKIE", None) + + def extract_wsgi( + self, + environ: "WSGIEnvironment", + headers: t.Union[Headers, t.List[t.Tuple[str, str]]], + ) -> None: + """Extract the server's set-cookie headers as cookies into the + cookie jar. + """ + self.extract_cookies( + _TestCookieResponse(headers), # type: ignore + _UrllibRequest(get_current_url(environ)), + ) + + +def _iter_data(data: t.Mapping[str, t.Any]) -> t.Iterator[t.Tuple[str, t.Any]]: + """Iterate over a mapping that might have a list of values, yielding + all key, value pairs. Almost like iter_multi_items but only allows + lists, not tuples, of values so tuples can be used for files. + """ + if isinstance(data, MultiDict): + yield from data.items(multi=True) + else: + for key, value in data.items(): + if isinstance(value, list): + for v in value: + yield key, v + else: + yield key, value + + +_TAnyMultiDict = t.TypeVar("_TAnyMultiDict", bound=MultiDict) + + +class EnvironBuilder: + """This class can be used to conveniently create a WSGI environment + for testing purposes. It can be used to quickly create WSGI environments + or request objects from arbitrary data. + + The signature of this class is also used in some other places as of + Werkzeug 0.5 (:func:`create_environ`, :meth:`Response.from_values`, + :meth:`Client.open`). Because of this most of the functionality is + available through the constructor alone. + + Files and regular form data can be manipulated independently of each + other with the :attr:`form` and :attr:`files` attributes, but are + passed with the same argument to the constructor: `data`. + + `data` can be any of these values: + + - a `str` or `bytes` object: The object is converted into an + :attr:`input_stream`, the :attr:`content_length` is set and you have to + provide a :attr:`content_type`. + - a `dict` or :class:`MultiDict`: The keys have to be strings. The values + have to be either any of the following objects, or a list of any of the + following objects: + + - a :class:`file`-like object: These are converted into + :class:`FileStorage` objects automatically. + - a `tuple`: The :meth:`~FileMultiDict.add_file` method is called + with the key and the unpacked `tuple` items as positional + arguments. + - a `str`: The string is set as form data for the associated key. + - a file-like object: The object content is loaded in memory and then + handled like a regular `str` or a `bytes`. + + :param path: the path of the request. In the WSGI environment this will + end up as `PATH_INFO`. If the `query_string` is not defined + and there is a question mark in the `path` everything after + it is used as query string. + :param base_url: the base URL is a URL that is used to extract the WSGI + URL scheme, host (server name + server port) and the + script root (`SCRIPT_NAME`). + :param query_string: an optional string or dict with URL parameters. + :param method: the HTTP method to use, defaults to `GET`. + :param input_stream: an optional input stream. Do not specify this and + `data`. As soon as an input stream is set you can't + modify :attr:`args` and :attr:`files` unless you + set the :attr:`input_stream` to `None` again. + :param content_type: The content type for the request. As of 0.5 you + don't have to provide this when specifying files + and form data via `data`. + :param content_length: The content length for the request. You don't + have to specify this when providing data via + `data`. + :param errors_stream: an optional error stream that is used for + `wsgi.errors`. Defaults to :data:`stderr`. + :param multithread: controls `wsgi.multithread`. Defaults to `False`. + :param multiprocess: controls `wsgi.multiprocess`. Defaults to `False`. + :param run_once: controls `wsgi.run_once`. Defaults to `False`. + :param headers: an optional list or :class:`Headers` object of headers. + :param data: a string or dict of form data or a file-object. + See explanation above. + :param json: An object to be serialized and assigned to ``data``. + Defaults the content type to ``"application/json"``. + Serialized with the function assigned to :attr:`json_dumps`. + :param environ_base: an optional dict of environment defaults. + :param environ_overrides: an optional dict of environment overrides. + :param charset: the charset used to encode string data. + :param auth: An authorization object to use for the + ``Authorization`` header value. A ``(username, password)`` tuple + is a shortcut for ``Basic`` authorization. + + .. versionchanged:: 2.0 + ``REQUEST_URI`` and ``RAW_URI`` is the full raw URI including + the query string, not only the path. + + .. versionchanged:: 2.0 + The default :attr:`request_class` is ``Request`` instead of + ``BaseRequest``. + + .. versionadded:: 2.0 + Added the ``auth`` parameter. + + .. versionadded:: 0.15 + The ``json`` param and :meth:`json_dumps` method. + + .. versionadded:: 0.15 + The environ has keys ``REQUEST_URI`` and ``RAW_URI`` containing + the path before perecent-decoding. This is not part of the WSGI + PEP, but many WSGI servers include it. + + .. versionchanged:: 0.6 + ``path`` and ``base_url`` can now be unicode strings that are + encoded with :func:`iri_to_uri`. + """ + + #: the server protocol to use. defaults to HTTP/1.1 + server_protocol = "HTTP/1.1" + + #: the wsgi version to use. defaults to (1, 0) + wsgi_version = (1, 0) + + #: The default request class used by :meth:`get_request`. + request_class = Request + + import json + + #: The serialization function used when ``json`` is passed. + json_dumps = staticmethod(json.dumps) + del json + + _args: t.Optional[MultiDict] + _query_string: t.Optional[str] + _input_stream: t.Optional[t.IO[bytes]] + _form: t.Optional[MultiDict] + _files: t.Optional[FileMultiDict] + + def __init__( + self, + path: str = "/", + base_url: t.Optional[str] = None, + query_string: t.Optional[t.Union[t.Mapping[str, str], str]] = None, + method: str = "GET", + input_stream: t.Optional[t.IO[bytes]] = None, + content_type: t.Optional[str] = None, + content_length: t.Optional[int] = None, + errors_stream: t.Optional[t.IO[str]] = None, + multithread: bool = False, + multiprocess: bool = False, + run_once: bool = False, + headers: t.Optional[t.Union[Headers, t.Iterable[t.Tuple[str, str]]]] = None, + data: t.Optional[ + t.Union[t.IO[bytes], str, bytes, t.Mapping[str, t.Any]] + ] = None, + environ_base: t.Optional[t.Mapping[str, t.Any]] = None, + environ_overrides: t.Optional[t.Mapping[str, t.Any]] = None, + charset: str = "utf-8", + mimetype: t.Optional[str] = None, + json: t.Optional[t.Mapping[str, t.Any]] = None, + auth: t.Optional[t.Union[Authorization, t.Tuple[str, str]]] = None, + ) -> None: + path_s = _make_encode_wrapper(path) + if query_string is not None and path_s("?") in path: + raise ValueError("Query string is defined in the path and as an argument") + request_uri = url_parse(path) + if query_string is None and path_s("?") in path: + query_string = request_uri.query + self.charset = charset + self.path = iri_to_uri(request_uri.path) + self.request_uri = path + if base_url is not None: + base_url = url_fix(iri_to_uri(base_url, charset), charset) + self.base_url = base_url # type: ignore + if isinstance(query_string, (bytes, str)): + self.query_string = query_string + else: + if query_string is None: + query_string = MultiDict() + elif not isinstance(query_string, MultiDict): + query_string = MultiDict(query_string) + self.args = query_string + self.method = method + if headers is None: + headers = Headers() + elif not isinstance(headers, Headers): + headers = Headers(headers) + self.headers = headers + if content_type is not None: + self.content_type = content_type + if errors_stream is None: + errors_stream = sys.stderr + self.errors_stream = errors_stream + self.multithread = multithread + self.multiprocess = multiprocess + self.run_once = run_once + self.environ_base = environ_base + self.environ_overrides = environ_overrides + self.input_stream = input_stream + self.content_length = content_length + self.closed = False + + if auth is not None: + if isinstance(auth, tuple): + auth = Authorization( + "basic", {"username": auth[0], "password": auth[1]} + ) + + self.headers.set("Authorization", auth.to_header()) + + if json is not None: + if data is not None: + raise TypeError("can't provide both json and data") + + data = self.json_dumps(json) + + if self.content_type is None: + self.content_type = "application/json" + + if data: + if input_stream is not None: + raise TypeError("can't provide input stream and data") + if hasattr(data, "read"): + data = data.read() # type: ignore + if isinstance(data, str): + data = data.encode(self.charset) + if isinstance(data, bytes): + self.input_stream = BytesIO(data) + if self.content_length is None: + self.content_length = len(data) + else: + for key, value in _iter_data(data): # type: ignore + if isinstance(value, (tuple, dict)) or hasattr(value, "read"): + self._add_file_from_data(key, value) + else: + self.form.setlistdefault(key).append(value) + + if mimetype is not None: + self.mimetype = mimetype + + @classmethod + def from_environ( + cls, environ: "WSGIEnvironment", **kwargs: t.Any + ) -> "EnvironBuilder": + """Turn an environ dict back into a builder. Any extra kwargs + override the args extracted from the environ. + + .. versionchanged:: 2.0 + Path and query values are passed through the WSGI decoding + dance to avoid double encoding. + + .. versionadded:: 0.15 + """ + headers = Headers(EnvironHeaders(environ)) + out = { + "path": _wsgi_decoding_dance(environ["PATH_INFO"]), + "base_url": cls._make_base_url( + environ["wsgi.url_scheme"], + headers.pop("Host"), + _wsgi_decoding_dance(environ["SCRIPT_NAME"]), + ), + "query_string": _wsgi_decoding_dance(environ["QUERY_STRING"]), + "method": environ["REQUEST_METHOD"], + "input_stream": environ["wsgi.input"], + "content_type": headers.pop("Content-Type", None), + "content_length": headers.pop("Content-Length", None), + "errors_stream": environ["wsgi.errors"], + "multithread": environ["wsgi.multithread"], + "multiprocess": environ["wsgi.multiprocess"], + "run_once": environ["wsgi.run_once"], + "headers": headers, + } + out.update(kwargs) + return cls(**out) + + def _add_file_from_data( + self, + key: str, + value: t.Union[ + t.IO[bytes], t.Tuple[t.IO[bytes], str], t.Tuple[t.IO[bytes], str, str] + ], + ) -> None: + """Called in the EnvironBuilder to add files from the data dict.""" + if isinstance(value, tuple): + self.files.add_file(key, *value) + else: + self.files.add_file(key, value) + + @staticmethod + def _make_base_url(scheme: str, host: str, script_root: str) -> str: + return url_unparse((scheme, host, script_root, "", "")).rstrip("/") + "/" + + @property + def base_url(self) -> str: + """The base URL is used to extract the URL scheme, host name, + port, and root path. + """ + return self._make_base_url(self.url_scheme, self.host, self.script_root) + + @base_url.setter + def base_url(self, value: t.Optional[str]) -> None: + if value is None: + scheme = "http" + netloc = "localhost" + script_root = "" + else: + scheme, netloc, script_root, qs, anchor = url_parse(value) + if qs or anchor: + raise ValueError("base url must not contain a query string or fragment") + self.script_root = script_root.rstrip("/") + self.host = netloc + self.url_scheme = scheme + + @property + def content_type(self) -> t.Optional[str]: + """The content type for the request. Reflected from and to + the :attr:`headers`. Do not set if you set :attr:`files` or + :attr:`form` for auto detection. + """ + ct = self.headers.get("Content-Type") + if ct is None and not self._input_stream: + if self._files: + return "multipart/form-data" + if self._form: + return "application/x-www-form-urlencoded" + return None + return ct + + @content_type.setter + def content_type(self, value: t.Optional[str]) -> None: + if value is None: + self.headers.pop("Content-Type", None) + else: + self.headers["Content-Type"] = value + + @property + def mimetype(self) -> t.Optional[str]: + """The mimetype (content type without charset etc.) + + .. versionadded:: 0.14 + """ + ct = self.content_type + return ct.split(";")[0].strip() if ct else None + + @mimetype.setter + def mimetype(self, value: str) -> None: + self.content_type = get_content_type(value, self.charset) + + @property + def mimetype_params(self) -> t.Mapping[str, str]: + """The mimetype parameters as dict. For example if the + content type is ``text/html; charset=utf-8`` the params would be + ``{'charset': 'utf-8'}``. + + .. versionadded:: 0.14 + """ + + def on_update(d: CallbackDict) -> None: + self.headers["Content-Type"] = dump_options_header(self.mimetype, d) + + d = parse_options_header(self.headers.get("content-type", ""))[1] + return CallbackDict(d, on_update) + + @property + def content_length(self) -> t.Optional[int]: + """The content length as integer. Reflected from and to the + :attr:`headers`. Do not set if you set :attr:`files` or + :attr:`form` for auto detection. + """ + return self.headers.get("Content-Length", type=int) + + @content_length.setter + def content_length(self, value: t.Optional[int]) -> None: + if value is None: + self.headers.pop("Content-Length", None) + else: + self.headers["Content-Length"] = str(value) + + def _get_form(self, name: str, storage: t.Type[_TAnyMultiDict]) -> _TAnyMultiDict: + """Common behavior for getting the :attr:`form` and + :attr:`files` properties. + + :param name: Name of the internal cached attribute. + :param storage: Storage class used for the data. + """ + if self.input_stream is not None: + raise AttributeError("an input stream is defined") + + rv = getattr(self, name) + + if rv is None: + rv = storage() + setattr(self, name, rv) + + return rv # type: ignore + + def _set_form(self, name: str, value: MultiDict) -> None: + """Common behavior for setting the :attr:`form` and + :attr:`files` properties. + + :param name: Name of the internal cached attribute. + :param value: Value to assign to the attribute. + """ + self._input_stream = None + setattr(self, name, value) + + @property + def form(self) -> MultiDict: + """A :class:`MultiDict` of form values.""" + return self._get_form("_form", MultiDict) + + @form.setter + def form(self, value: MultiDict) -> None: + self._set_form("_form", value) + + @property + def files(self) -> FileMultiDict: + """A :class:`FileMultiDict` of uploaded files. Use + :meth:`~FileMultiDict.add_file` to add new files. + """ + return self._get_form("_files", FileMultiDict) + + @files.setter + def files(self, value: FileMultiDict) -> None: + self._set_form("_files", value) + + @property + def input_stream(self) -> t.Optional[t.IO[bytes]]: + """An optional input stream. This is mutually exclusive with + setting :attr:`form` and :attr:`files`, setting it will clear + those. Do not provide this if the method is not ``POST`` or + another method that has a body. + """ + return self._input_stream + + @input_stream.setter + def input_stream(self, value: t.Optional[t.IO[bytes]]) -> None: + self._input_stream = value + self._form = None + self._files = None + + @property + def query_string(self) -> str: + """The query string. If you set this to a string + :attr:`args` will no longer be available. + """ + if self._query_string is None: + if self._args is not None: + return url_encode(self._args, charset=self.charset) + return "" + return self._query_string + + @query_string.setter + def query_string(self, value: t.Optional[str]) -> None: + self._query_string = value + self._args = None + + @property + def args(self) -> MultiDict: + """The URL arguments as :class:`MultiDict`.""" + if self._query_string is not None: + raise AttributeError("a query string is defined") + if self._args is None: + self._args = MultiDict() + return self._args + + @args.setter + def args(self, value: t.Optional[MultiDict]) -> None: + self._query_string = None + self._args = value + + @property + def server_name(self) -> str: + """The server name (read-only, use :attr:`host` to set)""" + return self.host.split(":", 1)[0] + + @property + def server_port(self) -> int: + """The server port as integer (read-only, use :attr:`host` to set)""" + pieces = self.host.split(":", 1) + if len(pieces) == 2 and pieces[1].isdigit(): + return int(pieces[1]) + if self.url_scheme == "https": + return 443 + return 80 + + def __del__(self) -> None: + try: + self.close() + except Exception: + pass + + def close(self) -> None: + """Closes all files. If you put real :class:`file` objects into the + :attr:`files` dict you can call this method to automatically close + them all in one go. + """ + if self.closed: + return + try: + files = self.files.values() + except AttributeError: + files = () # type: ignore + for f in files: + try: + f.close() + except Exception: + pass + self.closed = True + + def get_environ(self) -> "WSGIEnvironment": + """Return the built environ. + + .. versionchanged:: 0.15 + The content type and length headers are set based on + input stream detection. Previously this only set the WSGI + keys. + """ + input_stream = self.input_stream + content_length = self.content_length + + mimetype = self.mimetype + content_type = self.content_type + + if input_stream is not None: + start_pos = input_stream.tell() + input_stream.seek(0, 2) + end_pos = input_stream.tell() + input_stream.seek(start_pos) + content_length = end_pos - start_pos + elif mimetype == "multipart/form-data": + input_stream, content_length, boundary = stream_encode_multipart( + CombinedMultiDict([self.form, self.files]), charset=self.charset + ) + content_type = f'{mimetype}; boundary="{boundary}"' + elif mimetype == "application/x-www-form-urlencoded": + form_encoded = url_encode(self.form, charset=self.charset).encode("ascii") + content_length = len(form_encoded) + input_stream = BytesIO(form_encoded) + else: + input_stream = BytesIO() + + result: "WSGIEnvironment" = {} + if self.environ_base: + result.update(self.environ_base) + + def _path_encode(x: str) -> str: + return _wsgi_encoding_dance(url_unquote(x, self.charset), self.charset) + + raw_uri = _wsgi_encoding_dance(self.request_uri, self.charset) + result.update( + { + "REQUEST_METHOD": self.method, + "SCRIPT_NAME": _path_encode(self.script_root), + "PATH_INFO": _path_encode(self.path), + "QUERY_STRING": _wsgi_encoding_dance(self.query_string, self.charset), + # Non-standard, added by mod_wsgi, uWSGI + "REQUEST_URI": raw_uri, + # Non-standard, added by gunicorn + "RAW_URI": raw_uri, + "SERVER_NAME": self.server_name, + "SERVER_PORT": str(self.server_port), + "HTTP_HOST": self.host, + "SERVER_PROTOCOL": self.server_protocol, + "wsgi.version": self.wsgi_version, + "wsgi.url_scheme": self.url_scheme, + "wsgi.input": input_stream, + "wsgi.errors": self.errors_stream, + "wsgi.multithread": self.multithread, + "wsgi.multiprocess": self.multiprocess, + "wsgi.run_once": self.run_once, + } + ) + + headers = self.headers.copy() + + if content_type is not None: + result["CONTENT_TYPE"] = content_type + headers.set("Content-Type", content_type) + + if content_length is not None: + result["CONTENT_LENGTH"] = str(content_length) + headers.set("Content-Length", content_length) + + combined_headers = defaultdict(list) + + for key, value in headers.to_wsgi_list(): + combined_headers[f"HTTP_{key.upper().replace('-', '_')}"].append(value) + + for key, values in combined_headers.items(): + result[key] = ", ".join(values) + + if self.environ_overrides: + result.update(self.environ_overrides) + + return result + + def get_request(self, cls: t.Optional[t.Type[Request]] = None) -> Request: + """Returns a request with the data. If the request class is not + specified :attr:`request_class` is used. + + :param cls: The request wrapper to use. + """ + if cls is None: + cls = self.request_class + + return cls(self.get_environ()) + + +class ClientRedirectError(Exception): + """If a redirect loop is detected when using follow_redirects=True with + the :cls:`Client`, then this exception is raised. + """ + + +class Client: + """This class allows you to send requests to a wrapped application. + + The use_cookies parameter indicates whether cookies should be stored and + sent for subsequent requests. This is True by default, but passing False + will disable this behaviour. + + If you want to request some subdomain of your application you may set + `allow_subdomain_redirects` to `True` as if not no external redirects + are allowed. + + .. versionchanged:: 2.0 + ``response_wrapper`` is always a subclass of + :class:``TestResponse``. + + .. versionchanged:: 0.5 + Added the ``use_cookies`` parameter. + """ + + def __init__( + self, + application: "WSGIApplication", + response_wrapper: t.Optional[t.Type["Response"]] = None, + use_cookies: bool = True, + allow_subdomain_redirects: bool = False, + ) -> None: + self.application = application + + if response_wrapper in {None, Response}: + response_wrapper = TestResponse + elif not isinstance(response_wrapper, TestResponse): + response_wrapper = type( + "WrapperTestResponse", + (TestResponse, response_wrapper), # type: ignore + {}, + ) + + self.response_wrapper = t.cast(t.Type["TestResponse"], response_wrapper) + + if use_cookies: + self.cookie_jar: t.Optional[_TestCookieJar] = _TestCookieJar() + else: + self.cookie_jar = None + + self.allow_subdomain_redirects = allow_subdomain_redirects + + def set_cookie( + self, + server_name: str, + key: str, + value: str = "", + max_age: t.Optional[t.Union[timedelta, int]] = None, + expires: t.Optional[t.Union[str, datetime, int, float]] = None, + path: str = "/", + domain: t.Optional[str] = None, + secure: bool = False, + httponly: bool = False, + samesite: t.Optional[str] = None, + charset: str = "utf-8", + ) -> None: + """Sets a cookie in the client's cookie jar. The server name + is required and has to match the one that is also passed to + the open call. + """ + assert self.cookie_jar is not None, "cookies disabled" + header = dump_cookie( + key, + value, + max_age, + expires, + path, + domain, + secure, + httponly, + charset, + samesite=samesite, + ) + environ = create_environ(path, base_url=f"http://{server_name}") + headers = [("Set-Cookie", header)] + self.cookie_jar.extract_wsgi(environ, headers) + + def delete_cookie( + self, + server_name: str, + key: str, + path: str = "/", + domain: t.Optional[str] = None, + secure: bool = False, + httponly: bool = False, + samesite: t.Optional[str] = None, + ) -> None: + """Deletes a cookie in the test client.""" + self.set_cookie( + server_name, + key, + expires=0, + max_age=0, + path=path, + domain=domain, + secure=secure, + httponly=httponly, + samesite=samesite, + ) + + def run_wsgi_app( + self, environ: "WSGIEnvironment", buffered: bool = False + ) -> t.Tuple[t.Iterable[bytes], str, Headers]: + """Runs the wrapped WSGI app with the given environment. + + :meta private: + """ + if self.cookie_jar is not None: + self.cookie_jar.inject_wsgi(environ) + + rv = run_wsgi_app(self.application, environ, buffered=buffered) + + if self.cookie_jar is not None: + self.cookie_jar.extract_wsgi(environ, rv[2]) + + return rv + + def resolve_redirect( + self, response: "TestResponse", buffered: bool = False + ) -> "TestResponse": + """Perform a new request to the location given by the redirect + response to the previous request. + + :meta private: + """ + scheme, netloc, path, qs, anchor = url_parse(response.location) + builder = EnvironBuilder.from_environ(response.request.environ, query_string=qs) + + to_name_parts = netloc.split(":", 1)[0].split(".") + from_name_parts = builder.server_name.split(".") + + if to_name_parts != [""]: + # The new location has a host, use it for the base URL. + builder.url_scheme = scheme + builder.host = netloc + else: + # A local redirect with autocorrect_location_header=False + # doesn't have a host, so use the request's host. + to_name_parts = from_name_parts + + # Explain why a redirect to a different server name won't be followed. + if to_name_parts != from_name_parts: + if to_name_parts[-len(from_name_parts) :] == from_name_parts: + if not self.allow_subdomain_redirects: + raise RuntimeError("Following subdomain redirects is not enabled.") + else: + raise RuntimeError("Following external redirects is not supported.") + + path_parts = path.split("/") + root_parts = builder.script_root.split("/") + + if path_parts[: len(root_parts)] == root_parts: + # Strip the script root from the path. + builder.path = path[len(builder.script_root) :] + else: + # The new location is not under the script root, so use the + # whole path and clear the previous root. + builder.path = path + builder.script_root = "" + + # Only 307 and 308 preserve all of the original request. + if response.status_code not in {307, 308}: + # HEAD is preserved, everything else becomes GET. + if builder.method != "HEAD": + builder.method = "GET" + + # Clear the body and the headers that describe it. + + if builder.input_stream is not None: + builder.input_stream.close() + builder.input_stream = None + + builder.content_type = None + builder.content_length = None + builder.headers.pop("Transfer-Encoding", None) + + return self.open(builder, buffered=buffered) + + def open( + self, + *args: t.Any, + as_tuple: bool = False, + buffered: bool = False, + follow_redirects: bool = False, + **kwargs: t.Any, + ) -> "TestResponse": + """Generate an environ dict from the given arguments, make a + request to the application using it, and return the response. + + :param args: Passed to :class:`EnvironBuilder` to create the + environ for the request. If a single arg is passed, it can + be an existing :class:`EnvironBuilder` or an environ dict. + :param buffered: Convert the iterator returned by the app into + a list. If the iterator has a ``close()`` method, it is + called automatically. + :param follow_redirects: Make additional requests to follow HTTP + redirects until a non-redirect status is returned. + :attr:`TestResponse.history` lists the intermediate + responses. + + .. versionchanged:: 2.0 + ``as_tuple`` is deprecated and will be removed in Werkzeug + 2.1. Use :attr:`TestResponse.request` and + ``request.environ`` instead. + + .. versionchanged:: 2.0 + The request input stream is closed when calling + ``response.close()``. Input streams for redirects are + automatically closed. + + .. versionchanged:: 0.5 + If a dict is provided as file in the dict for the ``data`` + parameter the content type has to be called ``content_type`` + instead of ``mimetype``. This change was made for + consistency with :class:`werkzeug.FileWrapper`. + + .. versionchanged:: 0.5 + Added the ``follow_redirects`` parameter. + """ + request: t.Optional["Request"] = None + + if not kwargs and len(args) == 1: + arg = args[0] + + if isinstance(arg, EnvironBuilder): + request = arg.get_request() + elif isinstance(arg, dict): + request = EnvironBuilder.from_environ(arg).get_request() + elif isinstance(arg, Request): + request = arg + + if request is None: + builder = EnvironBuilder(*args, **kwargs) + + try: + request = builder.get_request() + finally: + builder.close() + + response = self.run_wsgi_app(request.environ, buffered=buffered) + response = self.response_wrapper(*response, request=request) + + redirects = set() + history: t.List["TestResponse"] = [] + + while follow_redirects and response.status_code in { + 301, + 302, + 303, + 305, + 307, + 308, + }: + # Exhaust intermediate response bodies to ensure middleware + # that returns an iterator runs any cleanup code. + if not buffered: + response.make_sequence() + response.close() + + new_redirect_entry = (response.location, response.status_code) + + if new_redirect_entry in redirects: + raise ClientRedirectError( + f"Loop detected: A {response.status_code} redirect" + f" to {response.location} was already made." + ) + + redirects.add(new_redirect_entry) + response.history = tuple(history) + history.append(response) + response = self.resolve_redirect(response, buffered=buffered) + else: + # This is the final request after redirects, or not + # following redirects. + response.history = tuple(history) + # Close the input stream when closing the response, in case + # the input is an open temporary file. + response.call_on_close(request.input_stream.close) + + if as_tuple: + warnings.warn( + "'as_tuple' is deprecated and will be removed in" + " Werkzeug 2.1. Access 'response.request.environ'" + " instead.", + DeprecationWarning, + stacklevel=2, + ) + return request.environ, response # type: ignore + + return response + + def get(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + """Call :meth:`open` with ``method`` set to ``GET``.""" + kw["method"] = "GET" + return self.open(*args, **kw) + + def post(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + """Call :meth:`open` with ``method`` set to ``POST``.""" + kw["method"] = "POST" + return self.open(*args, **kw) + + def put(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + """Call :meth:`open` with ``method`` set to ``PUT``.""" + kw["method"] = "PUT" + return self.open(*args, **kw) + + def delete(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + """Call :meth:`open` with ``method`` set to ``DELETE``.""" + kw["method"] = "DELETE" + return self.open(*args, **kw) + + def patch(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + """Call :meth:`open` with ``method`` set to ``PATCH``.""" + kw["method"] = "PATCH" + return self.open(*args, **kw) + + def options(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + """Call :meth:`open` with ``method`` set to ``OPTIONS``.""" + kw["method"] = "OPTIONS" + return self.open(*args, **kw) + + def head(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + """Call :meth:`open` with ``method`` set to ``HEAD``.""" + kw["method"] = "HEAD" + return self.open(*args, **kw) + + def trace(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + """Call :meth:`open` with ``method`` set to ``TRACE``.""" + kw["method"] = "TRACE" + return self.open(*args, **kw) + + def __repr__(self) -> str: + return f"<{type(self).__name__} {self.application!r}>" + + +def create_environ(*args: t.Any, **kwargs: t.Any) -> "WSGIEnvironment": + """Create a new WSGI environ dict based on the values passed. The first + parameter should be the path of the request which defaults to '/'. The + second one can either be an absolute path (in that case the host is + localhost:80) or a full path to the request with scheme, netloc port and + the path to the script. + + This accepts the same arguments as the :class:`EnvironBuilder` + constructor. + + .. versionchanged:: 0.5 + This function is now a thin wrapper over :class:`EnvironBuilder` which + was added in 0.5. The `headers`, `environ_base`, `environ_overrides` + and `charset` parameters were added. + """ + builder = EnvironBuilder(*args, **kwargs) + + try: + return builder.get_environ() + finally: + builder.close() + + +def run_wsgi_app( + app: "WSGIApplication", environ: "WSGIEnvironment", buffered: bool = False +) -> t.Tuple[t.Iterable[bytes], str, Headers]: + """Return a tuple in the form (app_iter, status, headers) of the + application output. This works best if you pass it an application that + returns an iterator all the time. + + Sometimes applications may use the `write()` callable returned + by the `start_response` function. This tries to resolve such edge + cases automatically. But if you don't get the expected output you + should set `buffered` to `True` which enforces buffering. + + If passed an invalid WSGI application the behavior of this function is + undefined. Never pass non-conforming WSGI applications to this function. + + :param app: the application to execute. + :param buffered: set to `True` to enforce buffering. + :return: tuple in the form ``(app_iter, status, headers)`` + """ + # Copy environ to ensure any mutations by the app (ProxyFix, for + # example) don't affect subsequent requests (such as redirects). + environ = _get_environ(environ).copy() + status: str + response: t.Optional[t.Tuple[str, t.List[t.Tuple[str, str]]]] = None + buffer: t.List[bytes] = [] + + def start_response(status, headers, exc_info=None): # type: ignore + nonlocal response + + if exc_info: + try: + raise exc_info[1].with_traceback(exc_info[2]) + finally: + exc_info = None + + response = (status, headers) + return buffer.append + + app_rv = app(environ, start_response) + close_func = getattr(app_rv, "close", None) + app_iter: t.Iterable[bytes] = iter(app_rv) + + # when buffering we emit the close call early and convert the + # application iterator into a regular list + if buffered: + try: + app_iter = list(app_iter) + finally: + if close_func is not None: + close_func() + + # otherwise we iterate the application iter until we have a response, chain + # the already received data with the already collected data and wrap it in + # a new `ClosingIterator` if we need to restore a `close` callable from the + # original return value. + else: + for item in app_iter: + buffer.append(item) + + if response is not None: + break + + if buffer: + app_iter = chain(buffer, app_iter) + + if close_func is not None and app_iter is not app_rv: + app_iter = ClosingIterator(app_iter, close_func) + + status, headers = response # type: ignore + return app_iter, status, Headers(headers) + + +class TestResponse(Response): + """:class:`~werkzeug.wrappers.Response` subclass that provides extra + information about requests made with the test :class:`Client`. + + Test client requests will always return an instance of this class. + If a custom response class is passed to the client, it is + subclassed along with this to support test information. + + If the test request included large files, or if the application is + serving a file, call :meth:`close` to close any open files and + prevent Python showing a ``ResourceWarning``. + """ + + request: Request + """A request object with the environ used to make the request that + resulted in this response. + """ + + history: t.Tuple["TestResponse", ...] + """A list of intermediate responses. Populated when the test request + is made with ``follow_redirects`` enabled. + """ + + def __init__( + self, + response: t.Iterable[bytes], + status: str, + headers: Headers, + request: Request, + history: t.Tuple["TestResponse"] = (), # type: ignore + **kwargs: t.Any, + ) -> None: + super().__init__(response, status, headers, **kwargs) + self.request = request + self.history = history + self._compat_tuple = response, status, headers + + def __iter__(self) -> t.Iterator: + warnings.warn( + ( + "The test client no longer returns a tuple, it returns" + " a 'TestResponse'. Tuple unpacking is deprecated and" + " will be removed in Werkzeug 2.1. Access the" + " attributes 'data', 'status', and 'headers' instead." + ), + DeprecationWarning, + stacklevel=2, + ) + return iter(self._compat_tuple) + + def __getitem__(self, item: int) -> t.Any: + warnings.warn( + ( + "The test client no longer returns a tuple, it returns" + " a 'TestResponse'. Item indexing is deprecated and" + " will be removed in Werkzeug 2.1. Access the" + " attributes 'data', 'status', and 'headers' instead." + ), + DeprecationWarning, + stacklevel=2, + ) + return self._compat_tuple[item] diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/testapp.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/testapp.py new file mode 100644 index 000000000..981f8878b --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/testapp.py @@ -0,0 +1,240 @@ +"""A small application that can be used to test a WSGI server and check +it for WSGI compliance. +""" +import base64 +import os +import sys +import typing as t +from html import escape +from textwrap import wrap + +from . import __version__ as _werkzeug_version +from .wrappers.request import Request +from .wrappers.response import Response + +if t.TYPE_CHECKING: + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIEnvironment + + +logo = Response( + base64.b64decode( + """ +R0lGODlhoACgAOMIAAEDACwpAEpCAGdgAJaKAM28AOnVAP3rAP///////// +//////////////////////yH5BAEKAAgALAAAAACgAKAAAAT+EMlJq704680R+F0ojmRpnuj0rWnrv +nB8rbRs33gu0bzu/0AObxgsGn3D5HHJbCUFyqZ0ukkSDlAidctNFg7gbI9LZlrBaHGtzAae0eloe25 +7w9EDOX2fst/xenyCIn5/gFqDiVVDV4aGeYiKkhSFjnCQY5OTlZaXgZp8nJ2ekaB0SQOjqphrpnOiq +ncEn65UsLGytLVmQ6m4sQazpbtLqL/HwpnER8bHyLrLOc3Oz8PRONPU1crXN9na263dMt/g4SzjMeX +m5yDpLqgG7OzJ4u8lT/P69ej3JPn69kHzN2OIAHkB9RUYSFCFQYQJFTIkCDBiwoXWGnowaLEjRm7+G +p9A7Hhx4rUkAUaSLJlxHMqVMD/aSycSZkyTplCqtGnRAM5NQ1Ly5OmzZc6gO4d6DGAUKA+hSocWYAo +SlM6oUWX2O/o0KdaVU5vuSQLAa0ADwQgMEMB2AIECZhVSnTno6spgbtXmHcBUrQACcc2FrTrWS8wAf +78cMFBgwIBgbN+qvTt3ayikRBk7BoyGAGABAdYyfdzRQGV3l4coxrqQ84GpUBmrdR3xNIDUPAKDBSA +ADIGDhhqTZIWaDcrVX8EsbNzbkvCOxG8bN5w8ly9H8jyTJHC6DFndQydbguh2e/ctZJFXRxMAqqPVA +tQH5E64SPr1f0zz7sQYjAHg0In+JQ11+N2B0XXBeeYZgBZFx4tqBToiTCPv0YBgQv8JqA6BEf6RhXx +w1ENhRBnWV8ctEX4Ul2zc3aVGcQNC2KElyTDYyYUWvShdjDyMOGMuFjqnII45aogPhz/CodUHFwaDx +lTgsaOjNyhGWJQd+lFoAGk8ObghI0kawg+EV5blH3dr+digkYuAGSaQZFHFz2P/cTaLmhF52QeSb45 +Jwxd+uSVGHlqOZpOeJpCFZ5J+rkAkFjQ0N1tah7JJSZUFNsrkeJUJMIBi8jyaEKIhKPomnC91Uo+NB +yyaJ5umnnpInIFh4t6ZSpGaAVmizqjpByDegYl8tPE0phCYrhcMWSv+uAqHfgH88ak5UXZmlKLVJhd +dj78s1Fxnzo6yUCrV6rrDOkluG+QzCAUTbCwf9SrmMLzK6p+OPHx7DF+bsfMRq7Ec61Av9i6GLw23r +idnZ+/OO0a99pbIrJkproCQMA17OPG6suq3cca5ruDfXCCDoS7BEdvmJn5otdqscn+uogRHHXs8cbh +EIfYaDY1AkrC0cqwcZpnM6ludx72x0p7Fo/hZAcpJDjax0UdHavMKAbiKltMWCF3xxh9k25N/Viud8 +ba78iCvUkt+V6BpwMlErmcgc502x+u1nSxJSJP9Mi52awD1V4yB/QHONsnU3L+A/zR4VL/indx/y64 +gqcj+qgTeweM86f0Qy1QVbvmWH1D9h+alqg254QD8HJXHvjQaGOqEqC22M54PcftZVKVSQG9jhkv7C +JyTyDoAJfPdu8v7DRZAxsP/ky9MJ3OL36DJfCFPASC3/aXlfLOOON9vGZZHydGf8LnxYJuuVIbl83y +Az5n/RPz07E+9+zw2A2ahz4HxHo9Kt79HTMx1Q7ma7zAzHgHqYH0SoZWyTuOLMiHwSfZDAQTn0ajk9 +YQqodnUYjByQZhZak9Wu4gYQsMyEpIOAOQKze8CmEF45KuAHTvIDOfHJNipwoHMuGHBnJElUoDmAyX +c2Qm/R8Ah/iILCCJOEokGowdhDYc/yoL+vpRGwyVSCWFYZNljkhEirGXsalWcAgOdeAdoXcktF2udb +qbUhjWyMQxYO01o6KYKOr6iK3fE4MaS+DsvBsGOBaMb0Y6IxADaJhFICaOLmiWTlDAnY1KzDG4ambL +cWBA8mUzjJsN2KjSaSXGqMCVXYpYkj33mcIApyhQf6YqgeNAmNvuC0t4CsDbSshZJkCS1eNisKqlyG +cF8G2JeiDX6tO6Mv0SmjCa3MFb0bJaGPMU0X7c8XcpvMaOQmCajwSeY9G0WqbBmKv34DsMIEztU6Y2 +KiDlFdt6jnCSqx7Dmt6XnqSKaFFHNO5+FmODxMCWBEaco77lNDGXBM0ECYB/+s7nKFdwSF5hgXumQe +EZ7amRg39RHy3zIjyRCykQh8Zo2iviRKyTDn/zx6EefptJj2Cw+Ep2FSc01U5ry4KLPYsTyWnVGnvb +UpyGlhjBUljyjHhWpf8OFaXwhp9O4T1gU9UeyPPa8A2l0p1kNqPXEVRm1AOs1oAGZU596t6SOR2mcB +Oco1srWtkaVrMUzIErrKri85keKqRQYX9VX0/eAUK1hrSu6HMEX3Qh2sCh0q0D2CtnUqS4hj62sE/z +aDs2Sg7MBS6xnQeooc2R2tC9YrKpEi9pLXfYXp20tDCpSP8rKlrD4axprb9u1Df5hSbz9QU0cRpfgn +kiIzwKucd0wsEHlLpe5yHXuc6FrNelOl7pY2+11kTWx7VpRu97dXA3DO1vbkhcb4zyvERYajQgAADs +=""" + ), + mimetype="image/png", +) + + +TEMPLATE = """\ + +WSGI Information + +
    + +

    WSGI Information

    +

    + This page displays all available information about the WSGI server and + the underlying Python interpreter. +

    Python Interpreter

    + + + + + + +
    Python Version + %(python_version)s +
    Platform + %(platform)s [%(os)s] +
    API Version + %(api_version)s +
    Byteorder + %(byteorder)s +
    Werkzeug Version + %(werkzeug_version)s +
    +

    WSGI Environment

    + %(wsgi_env)s
    +

    Installed Eggs

    +

    + The following python packages were installed on the system as + Python eggs: +

      %(python_eggs)s
    +

    System Path

    +

    + The following paths are the current contents of the load path. The + following entries are looked up for Python packages. Note that not + all items in this path are folders. Gray and underlined items are + entries pointing to invalid resources or used by custom import hooks + such as the zip importer. +

    + Items with a bright background were expanded for display from a relative + path. If you encounter such paths in the output you might want to check + your setup as relative paths are usually problematic in multithreaded + environments. +

      %(sys_path)s
    +
    +""" + + +def iter_sys_path() -> t.Iterator[t.Tuple[str, bool, bool]]: + if os.name == "posix": + + def strip(x: str) -> str: + prefix = os.path.expanduser("~") + if x.startswith(prefix): + x = f"~{x[len(prefix) :]}" + return x + + else: + + def strip(x: str) -> str: + return x + + cwd = os.path.abspath(os.getcwd()) + for item in sys.path: + path = os.path.join(cwd, item or os.path.curdir) + yield strip(os.path.normpath(path)), not os.path.isdir(path), path != item + + +def render_testapp(req: Request) -> bytes: + try: + import pkg_resources + except ImportError: + eggs: t.Iterable[t.Any] = () + else: + eggs = sorted( + pkg_resources.working_set, + key=lambda x: x.project_name.lower(), # type: ignore + ) + python_eggs = [] + for egg in eggs: + try: + version = egg.version + except (ValueError, AttributeError): + version = "unknown" + python_eggs.append( + f"
  • {escape(egg.project_name)} [{escape(version)}]" + ) + + wsgi_env = [] + sorted_environ = sorted(req.environ.items(), key=lambda x: repr(x[0]).lower()) + for key, value in sorted_environ: + value = "".join(wrap(escape(repr(value)))) + wsgi_env.append(f"{escape(str(key))}{value}") + + sys_path = [] + for item, virtual, expanded in iter_sys_path(): + class_ = [] + if virtual: + class_.append("virtual") + if expanded: + class_.append("exp") + class_ = f' class="{" ".join(class_)}"' if class_ else "" + sys_path.append(f"{escape(item)}") + + return ( + TEMPLATE + % { + "python_version": "
    ".join(escape(sys.version).splitlines()), + "platform": escape(sys.platform), + "os": escape(os.name), + "api_version": sys.api_version, + "byteorder": sys.byteorder, + "werkzeug_version": _werkzeug_version, + "python_eggs": "\n".join(python_eggs), + "wsgi_env": "\n".join(wsgi_env), + "sys_path": "\n".join(sys_path), + } + ).encode("utf-8") + + +def test_app( + environ: "WSGIEnvironment", start_response: "StartResponse" +) -> t.Iterable[bytes]: + """Simple test application that dumps the environment. You can use + it to check if Werkzeug is working properly: + + .. sourcecode:: pycon + + >>> from werkzeug.serving import run_simple + >>> from werkzeug.testapp import test_app + >>> run_simple('localhost', 3000, test_app) + * Running on http://localhost:3000/ + + The application displays important information from the WSGI environment, + the Python interpreter and the installed libraries. + """ + req = Request(environ, populate_request=False) + if req.args.get("resource") == "logo": + response = logo + else: + response = Response(render_testapp(req), mimetype="text/html") + return response(environ, start_response) + + +if __name__ == "__main__": + from .serving import run_simple + + run_simple("localhost", 5000, test_app, use_reloader=True) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/urls.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/urls.py new file mode 100644 index 000000000..9529da0c7 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/urls.py @@ -0,0 +1,1211 @@ +"""Functions for working with URLs. + +Contains implementations of functions from :mod:`urllib.parse` that +handle bytes and strings. +""" +import codecs +import os +import re +import typing as t +import warnings + +from ._internal import _check_str_tuple +from ._internal import _decode_idna +from ._internal import _encode_idna +from ._internal import _make_encode_wrapper +from ._internal import _to_str + +if t.TYPE_CHECKING: + from . import datastructures as ds + +# A regular expression for what a valid schema looks like +_scheme_re = re.compile(r"^[a-zA-Z0-9+-.]+$") + +# Characters that are safe in any part of an URL. +_always_safe = frozenset( + bytearray( + b"abcdefghijklmnopqrstuvwxyz" + b"ABCDEFGHIJKLMNOPQRSTUVWXYZ" + b"0123456789" + b"-._~" + ) +) + +_hexdigits = "0123456789ABCDEFabcdef" +_hextobyte = { + f"{a}{b}".encode("ascii"): int(f"{a}{b}", 16) + for a in _hexdigits + for b in _hexdigits +} +_bytetohex = [f"%{char:02X}".encode("ascii") for char in range(256)] + + +class _URLTuple(t.NamedTuple): + scheme: str + netloc: str + path: str + query: str + fragment: str + + +class BaseURL(_URLTuple): + """Superclass of :py:class:`URL` and :py:class:`BytesURL`.""" + + __slots__ = () + _at: str + _colon: str + _lbracket: str + _rbracket: str + + def __str__(self) -> str: + return self.to_url() + + def replace(self, **kwargs: t.Any) -> "BaseURL": + """Return an URL with the same values, except for those parameters + given new values by whichever keyword arguments are specified.""" + return self._replace(**kwargs) + + @property + def host(self) -> t.Optional[str]: + """The host part of the URL if available, otherwise `None`. The + host is either the hostname or the IP address mentioned in the + URL. It will not contain the port. + """ + return self._split_host()[0] + + @property + def ascii_host(self) -> t.Optional[str]: + """Works exactly like :attr:`host` but will return a result that + is restricted to ASCII. If it finds a netloc that is not ASCII + it will attempt to idna decode it. This is useful for socket + operations when the URL might include internationalized characters. + """ + rv = self.host + if rv is not None and isinstance(rv, str): + try: + rv = _encode_idna(rv) # type: ignore + except UnicodeError: + rv = rv.encode("ascii", "ignore") # type: ignore + return _to_str(rv, "ascii", "ignore") + + @property + def port(self) -> t.Optional[int]: + """The port in the URL as an integer if it was present, `None` + otherwise. This does not fill in default ports. + """ + try: + rv = int(_to_str(self._split_host()[1])) + if 0 <= rv <= 65535: + return rv + except (ValueError, TypeError): + pass + return None + + @property + def auth(self) -> t.Optional[str]: + """The authentication part in the URL if available, `None` + otherwise. + """ + return self._split_netloc()[0] + + @property + def username(self) -> t.Optional[str]: + """The username if it was part of the URL, `None` otherwise. + This undergoes URL decoding and will always be a string. + """ + rv = self._split_auth()[0] + if rv is not None: + return _url_unquote_legacy(rv) + return None + + @property + def raw_username(self) -> t.Optional[str]: + """The username if it was part of the URL, `None` otherwise. + Unlike :attr:`username` this one is not being decoded. + """ + return self._split_auth()[0] + + @property + def password(self) -> t.Optional[str]: + """The password if it was part of the URL, `None` otherwise. + This undergoes URL decoding and will always be a string. + """ + rv = self._split_auth()[1] + if rv is not None: + return _url_unquote_legacy(rv) + return None + + @property + def raw_password(self) -> t.Optional[str]: + """The password if it was part of the URL, `None` otherwise. + Unlike :attr:`password` this one is not being decoded. + """ + return self._split_auth()[1] + + def decode_query(self, *args: t.Any, **kwargs: t.Any) -> "ds.MultiDict[str, str]": + """Decodes the query part of the URL. Ths is a shortcut for + calling :func:`url_decode` on the query argument. The arguments and + keyword arguments are forwarded to :func:`url_decode` unchanged. + """ + return url_decode(self.query, *args, **kwargs) + + def join(self, *args: t.Any, **kwargs: t.Any) -> "BaseURL": + """Joins this URL with another one. This is just a convenience + function for calling into :meth:`url_join` and then parsing the + return value again. + """ + return url_parse(url_join(self, *args, **kwargs)) + + def to_url(self) -> str: + """Returns a URL string or bytes depending on the type of the + information stored. This is just a convenience function + for calling :meth:`url_unparse` for this URL. + """ + return url_unparse(self) + + def encode_netloc(self) -> str: + """Encodes the netloc part to an ASCII safe URL as bytes.""" + rv = self.ascii_host or "" + if ":" in rv: + rv = f"[{rv}]" + port = self.port + if port is not None: + rv = f"{rv}:{port}" + auth = ":".join( + filter( + None, + [ + url_quote(self.raw_username or "", "utf-8", "strict", "/:%"), + url_quote(self.raw_password or "", "utf-8", "strict", "/:%"), + ], + ) + ) + if auth: + rv = f"{auth}@{rv}" + return rv + + def decode_netloc(self) -> str: + """Decodes the netloc part into a string.""" + rv = _decode_idna(self.host or "") + + if ":" in rv: + rv = f"[{rv}]" + port = self.port + if port is not None: + rv = f"{rv}:{port}" + auth = ":".join( + filter( + None, + [ + _url_unquote_legacy(self.raw_username or "", "/:%@"), + _url_unquote_legacy(self.raw_password or "", "/:%@"), + ], + ) + ) + if auth: + rv = f"{auth}@{rv}" + return rv + + def to_uri_tuple(self) -> "BaseURL": + """Returns a :class:`BytesURL` tuple that holds a URI. This will + encode all the information in the URL properly to ASCII using the + rules a web browser would follow. + + It's usually more interesting to directly call :meth:`iri_to_uri` which + will return a string. + """ + return url_parse(iri_to_uri(self)) + + def to_iri_tuple(self) -> "BaseURL": + """Returns a :class:`URL` tuple that holds a IRI. This will try + to decode as much information as possible in the URL without + losing information similar to how a web browser does it for the + URL bar. + + It's usually more interesting to directly call :meth:`uri_to_iri` which + will return a string. + """ + return url_parse(uri_to_iri(self)) + + def get_file_location( + self, pathformat: t.Optional[str] = None + ) -> t.Tuple[t.Optional[str], t.Optional[str]]: + """Returns a tuple with the location of the file in the form + ``(server, location)``. If the netloc is empty in the URL or + points to localhost, it's represented as ``None``. + + The `pathformat` by default is autodetection but needs to be set + when working with URLs of a specific system. The supported values + are ``'windows'`` when working with Windows or DOS paths and + ``'posix'`` when working with posix paths. + + If the URL does not point to a local file, the server and location + are both represented as ``None``. + + :param pathformat: The expected format of the path component. + Currently ``'windows'`` and ``'posix'`` are + supported. Defaults to ``None`` which is + autodetect. + """ + if self.scheme != "file": + return None, None + + path = url_unquote(self.path) + host = self.netloc or None + + if pathformat is None: + if os.name == "nt": + pathformat = "windows" + else: + pathformat = "posix" + + if pathformat == "windows": + if path[:1] == "/" and path[1:2].isalpha() and path[2:3] in "|:": + path = f"{path[1:2]}:{path[3:]}" + windows_share = path[:3] in ("\\" * 3, "/" * 3) + import ntpath + + path = ntpath.normpath(path) + # Windows shared drives are represented as ``\\host\\directory``. + # That results in a URL like ``file://///host/directory``, and a + # path like ``///host/directory``. We need to special-case this + # because the path contains the hostname. + if windows_share and host is None: + parts = path.lstrip("\\").split("\\", 1) + if len(parts) == 2: + host, path = parts + else: + host = parts[0] + path = "" + elif pathformat == "posix": + import posixpath + + path = posixpath.normpath(path) + else: + raise TypeError(f"Invalid path format {pathformat!r}") + + if host in ("127.0.0.1", "::1", "localhost"): + host = None + + return host, path + + def _split_netloc(self) -> t.Tuple[t.Optional[str], str]: + if self._at in self.netloc: + auth, _, netloc = self.netloc.partition(self._at) + return auth, netloc + return None, self.netloc + + def _split_auth(self) -> t.Tuple[t.Optional[str], t.Optional[str]]: + auth = self._split_netloc()[0] + if not auth: + return None, None + if self._colon not in auth: + return auth, None + + username, _, password = auth.partition(self._colon) + return username, password + + def _split_host(self) -> t.Tuple[t.Optional[str], t.Optional[str]]: + rv = self._split_netloc()[1] + if not rv: + return None, None + + if not rv.startswith(self._lbracket): + if self._colon in rv: + host, _, port = rv.partition(self._colon) + return host, port + return rv, None + + idx = rv.find(self._rbracket) + if idx < 0: + return rv, None + + host = rv[1:idx] + rest = rv[idx + 1 :] + if rest.startswith(self._colon): + return host, rest[1:] + return host, None + + +class URL(BaseURL): + """Represents a parsed URL. This behaves like a regular tuple but + also has some extra attributes that give further insight into the + URL. + """ + + __slots__ = () + _at = "@" + _colon = ":" + _lbracket = "[" + _rbracket = "]" + + def encode(self, charset: str = "utf-8", errors: str = "replace") -> "BytesURL": + """Encodes the URL to a tuple made out of bytes. The charset is + only being used for the path, query and fragment. + """ + return BytesURL( + self.scheme.encode("ascii"), # type: ignore + self.encode_netloc(), + self.path.encode(charset, errors), # type: ignore + self.query.encode(charset, errors), # type: ignore + self.fragment.encode(charset, errors), # type: ignore + ) + + +class BytesURL(BaseURL): + """Represents a parsed URL in bytes.""" + + __slots__ = () + _at = b"@" # type: ignore + _colon = b":" # type: ignore + _lbracket = b"[" # type: ignore + _rbracket = b"]" # type: ignore + + def __str__(self) -> str: + return self.to_url().decode("utf-8", "replace") # type: ignore + + def encode_netloc(self) -> bytes: # type: ignore + """Returns the netloc unchanged as bytes.""" + return self.netloc # type: ignore + + def decode(self, charset: str = "utf-8", errors: str = "replace") -> "URL": + """Decodes the URL to a tuple made out of strings. The charset is + only being used for the path, query and fragment. + """ + return URL( + self.scheme.decode("ascii"), # type: ignore + self.decode_netloc(), + self.path.decode(charset, errors), # type: ignore + self.query.decode(charset, errors), # type: ignore + self.fragment.decode(charset, errors), # type: ignore + ) + + +_unquote_maps: t.Dict[t.FrozenSet[int], t.Dict[bytes, int]] = {frozenset(): _hextobyte} + + +def _unquote_to_bytes( + string: t.Union[str, bytes], unsafe: t.Union[str, bytes] = "" +) -> bytes: + if isinstance(string, str): + string = string.encode("utf-8") + + if isinstance(unsafe, str): + unsafe = unsafe.encode("utf-8") + + unsafe = frozenset(bytearray(unsafe)) + groups = iter(string.split(b"%")) + result = bytearray(next(groups, b"")) + + try: + hex_to_byte = _unquote_maps[unsafe] + except KeyError: + hex_to_byte = _unquote_maps[unsafe] = { + h: b for h, b in _hextobyte.items() if b not in unsafe + } + + for group in groups: + code = group[:2] + + if code in hex_to_byte: + result.append(hex_to_byte[code]) + result.extend(group[2:]) + else: + result.append(37) # % + result.extend(group) + + return bytes(result) + + +def _url_encode_impl( + obj: t.Union[t.Mapping[str, str], t.Iterable[t.Tuple[str, str]]], + charset: str, + sort: bool, + key: t.Optional[t.Callable[[t.Tuple[str, str]], t.Any]], +) -> t.Iterator[str]: + from .datastructures import iter_multi_items + + iterable: t.Iterable[t.Tuple[str, str]] = iter_multi_items(obj) + + if sort: + iterable = sorted(iterable, key=key) + + for key_str, value_str in iterable: + if value_str is None: + continue + + if not isinstance(key_str, bytes): + key_bytes = str(key_str).encode(charset) + else: + key_bytes = key_str + + if not isinstance(value_str, bytes): + value_bytes = str(value_str).encode(charset) + else: + value_bytes = value_str + + yield f"{_fast_url_quote_plus(key_bytes)}={_fast_url_quote_plus(value_bytes)}" + + +def _url_unquote_legacy(value: str, unsafe: str = "") -> str: + try: + return url_unquote(value, charset="utf-8", errors="strict", unsafe=unsafe) + except UnicodeError: + return url_unquote(value, charset="latin1", unsafe=unsafe) + + +def url_parse( + url: str, scheme: t.Optional[str] = None, allow_fragments: bool = True +) -> BaseURL: + """Parses a URL from a string into a :class:`URL` tuple. If the URL + is lacking a scheme it can be provided as second argument. Otherwise, + it is ignored. Optionally fragments can be stripped from the URL + by setting `allow_fragments` to `False`. + + The inverse of this function is :func:`url_unparse`. + + :param url: the URL to parse. + :param scheme: the default schema to use if the URL is schemaless. + :param allow_fragments: if set to `False` a fragment will be removed + from the URL. + """ + s = _make_encode_wrapper(url) + is_text_based = isinstance(url, str) + + if scheme is None: + scheme = s("") + netloc = query = fragment = s("") + i = url.find(s(":")) + if i > 0 and _scheme_re.match(_to_str(url[:i], errors="replace")): + # make sure "iri" is not actually a port number (in which case + # "scheme" is really part of the path) + rest = url[i + 1 :] + if not rest or any(c not in s("0123456789") for c in rest): + # not a port number + scheme, url = url[:i].lower(), rest + + if url[:2] == s("//"): + delim = len(url) + for c in s("/?#"): + wdelim = url.find(c, 2) + if wdelim >= 0: + delim = min(delim, wdelim) + netloc, url = url[2:delim], url[delim:] + if (s("[") in netloc and s("]") not in netloc) or ( + s("]") in netloc and s("[") not in netloc + ): + raise ValueError("Invalid IPv6 URL") + + if allow_fragments and s("#") in url: + url, fragment = url.split(s("#"), 1) + if s("?") in url: + url, query = url.split(s("?"), 1) + + result_type = URL if is_text_based else BytesURL + return result_type(scheme, netloc, url, query, fragment) + + +def _make_fast_url_quote( + charset: str = "utf-8", + errors: str = "strict", + safe: t.Union[str, bytes] = "/:", + unsafe: t.Union[str, bytes] = "", +) -> t.Callable[[bytes], str]: + """Precompile the translation table for a URL encoding function. + + Unlike :func:`url_quote`, the generated function only takes the + string to quote. + + :param charset: The charset to encode the result with. + :param errors: How to handle encoding errors. + :param safe: An optional sequence of safe characters to never encode. + :param unsafe: An optional sequence of unsafe characters to always encode. + """ + if isinstance(safe, str): + safe = safe.encode(charset, errors) + + if isinstance(unsafe, str): + unsafe = unsafe.encode(charset, errors) + + safe = (frozenset(bytearray(safe)) | _always_safe) - frozenset(bytearray(unsafe)) + table = [chr(c) if c in safe else f"%{c:02X}" for c in range(256)] + + def quote(string: bytes) -> str: + return "".join([table[c] for c in string]) + + return quote + + +_fast_url_quote = _make_fast_url_quote() +_fast_quote_plus = _make_fast_url_quote(safe=" ", unsafe="+") + + +def _fast_url_quote_plus(string: bytes) -> str: + return _fast_quote_plus(string).replace(" ", "+") + + +def url_quote( + string: t.Union[str, bytes], + charset: str = "utf-8", + errors: str = "strict", + safe: t.Union[str, bytes] = "/:", + unsafe: t.Union[str, bytes] = "", +) -> str: + """URL encode a single string with a given encoding. + + :param s: the string to quote. + :param charset: the charset to be used. + :param safe: an optional sequence of safe characters. + :param unsafe: an optional sequence of unsafe characters. + + .. versionadded:: 0.9.2 + The `unsafe` parameter was added. + """ + if not isinstance(string, (str, bytes, bytearray)): + string = str(string) + if isinstance(string, str): + string = string.encode(charset, errors) + if isinstance(safe, str): + safe = safe.encode(charset, errors) + if isinstance(unsafe, str): + unsafe = unsafe.encode(charset, errors) + safe = (frozenset(bytearray(safe)) | _always_safe) - frozenset(bytearray(unsafe)) + rv = bytearray() + for char in bytearray(string): + if char in safe: + rv.append(char) + else: + rv.extend(_bytetohex[char]) + return bytes(rv).decode(charset) + + +def url_quote_plus( + string: str, charset: str = "utf-8", errors: str = "strict", safe: str = "" +) -> str: + """URL encode a single string with the given encoding and convert + whitespace to "+". + + :param s: The string to quote. + :param charset: The charset to be used. + :param safe: An optional sequence of safe characters. + """ + return url_quote(string, charset, errors, safe + " ", "+").replace(" ", "+") + + +def url_unparse(components: t.Tuple[str, str, str, str, str]) -> str: + """The reverse operation to :meth:`url_parse`. This accepts arbitrary + as well as :class:`URL` tuples and returns a URL as a string. + + :param components: the parsed URL as tuple which should be converted + into a URL string. + """ + _check_str_tuple(components) + scheme, netloc, path, query, fragment = components + s = _make_encode_wrapper(scheme) + url = s("") + + # We generally treat file:///x and file:/x the same which is also + # what browsers seem to do. This also allows us to ignore a schema + # register for netloc utilization or having to differentiate between + # empty and missing netloc. + if netloc or (scheme and path.startswith(s("/"))): + if path and path[:1] != s("/"): + path = s("/") + path + url = s("//") + (netloc or s("")) + path + elif path: + url += path + if scheme: + url = scheme + s(":") + url + if query: + url = url + s("?") + query + if fragment: + url = url + s("#") + fragment + return url + + +def url_unquote( + s: t.Union[str, bytes], + charset: str = "utf-8", + errors: str = "replace", + unsafe: str = "", +) -> str: + """URL decode a single string with a given encoding. If the charset + is set to `None` no decoding is performed and raw bytes are + returned. + + :param s: the string to unquote. + :param charset: the charset of the query string. If set to `None` + no decoding will take place. + :param errors: the error handling for the charset decoding. + """ + rv = _unquote_to_bytes(s, unsafe) + if charset is None: + return rv + return rv.decode(charset, errors) + + +def url_unquote_plus( + s: t.Union[str, bytes], charset: str = "utf-8", errors: str = "replace" +) -> str: + """URL decode a single string with the given `charset` and decode "+" to + whitespace. + + Per default encoding errors are ignored. If you want a different behavior + you can set `errors` to ``'replace'`` or ``'strict'``. + + :param s: The string to unquote. + :param charset: the charset of the query string. If set to `None` + no decoding will take place. + :param errors: The error handling for the `charset` decoding. + """ + if isinstance(s, str): + s = s.replace("+", " ") + else: + s = s.replace(b"+", b" ") + return url_unquote(s, charset, errors) + + +def url_fix(s: str, charset: str = "utf-8") -> str: + r"""Sometimes you get an URL by a user that just isn't a real URL because + it contains unsafe characters like ' ' and so on. This function can fix + some of the problems in a similar way browsers handle data entered by the + user: + + >>> url_fix('http://de.wikipedia.org/wiki/Elf (Begriffskl\xe4rung)') + 'http://de.wikipedia.org/wiki/Elf%20(Begriffskl%C3%A4rung)' + + :param s: the string with the URL to fix. + :param charset: The target charset for the URL if the url was given + as a string. + """ + # First step is to switch to text processing and to convert + # backslashes (which are invalid in URLs anyways) to slashes. This is + # consistent with what Chrome does. + s = _to_str(s, charset, "replace").replace("\\", "/") + + # For the specific case that we look like a malformed windows URL + # we want to fix this up manually: + if s.startswith("file://") and s[7:8].isalpha() and s[8:10] in (":/", "|/"): + s = f"file:///{s[7:]}" + + url = url_parse(s) + path = url_quote(url.path, charset, safe="/%+$!*'(),") + qs = url_quote_plus(url.query, charset, safe=":&%=+$!*'(),") + anchor = url_quote_plus(url.fragment, charset, safe=":&%=+$!*'(),") + return url_unparse((url.scheme, url.encode_netloc(), path, qs, anchor)) + + +# not-unreserved characters remain quoted when unquoting to IRI +_to_iri_unsafe = "".join([chr(c) for c in range(128) if c not in _always_safe]) + + +def _codec_error_url_quote(e: UnicodeError) -> t.Tuple[str, int]: + """Used in :func:`uri_to_iri` after unquoting to re-quote any + invalid bytes. + """ + # the docs state that UnicodeError does have these attributes, + # but mypy isn't picking them up + out = _fast_url_quote(e.object[e.start : e.end]) # type: ignore + return out, e.end # type: ignore + + +codecs.register_error("werkzeug.url_quote", _codec_error_url_quote) + + +def uri_to_iri( + uri: t.Union[str, t.Tuple[str, str, str, str, str]], + charset: str = "utf-8", + errors: str = "werkzeug.url_quote", +) -> str: + """Convert a URI to an IRI. All valid UTF-8 characters are unquoted, + leaving all reserved and invalid characters quoted. If the URL has + a domain, it is decoded from Punycode. + + >>> uri_to_iri("http://xn--n3h.net/p%C3%A5th?q=%C3%A8ry%DF") + 'http://\\u2603.net/p\\xe5th?q=\\xe8ry%DF' + + :param uri: The URI to convert. + :param charset: The encoding to encode unquoted bytes with. + :param errors: Error handler to use during ``bytes.encode``. By + default, invalid bytes are left quoted. + + .. versionchanged:: 0.15 + All reserved and invalid characters remain quoted. Previously, + only some reserved characters were preserved, and invalid bytes + were replaced instead of left quoted. + + .. versionadded:: 0.6 + """ + if isinstance(uri, tuple): + uri = url_unparse(uri) + + uri = url_parse(_to_str(uri, charset)) + path = url_unquote(uri.path, charset, errors, _to_iri_unsafe) + query = url_unquote(uri.query, charset, errors, _to_iri_unsafe) + fragment = url_unquote(uri.fragment, charset, errors, _to_iri_unsafe) + return url_unparse((uri.scheme, uri.decode_netloc(), path, query, fragment)) + + +# reserved characters remain unquoted when quoting to URI +_to_uri_safe = ":/?#[]@!$&'()*+,;=%" + + +def iri_to_uri( + iri: t.Union[str, t.Tuple[str, str, str, str, str]], + charset: str = "utf-8", + errors: str = "strict", + safe_conversion: bool = False, +) -> str: + """Convert an IRI to a URI. All non-ASCII and unsafe characters are + quoted. If the URL has a domain, it is encoded to Punycode. + + >>> iri_to_uri('http://\\u2603.net/p\\xe5th?q=\\xe8ry%DF') + 'http://xn--n3h.net/p%C3%A5th?q=%C3%A8ry%DF' + + :param iri: The IRI to convert. + :param charset: The encoding of the IRI. + :param errors: Error handler to use during ``bytes.encode``. + :param safe_conversion: Return the URL unchanged if it only contains + ASCII characters and no whitespace. See the explanation below. + + There is a general problem with IRI conversion with some protocols + that are in violation of the URI specification. Consider the + following two IRIs:: + + magnet:?xt=uri:whatever + itms-services://?action=download-manifest + + After parsing, we don't know if the scheme requires the ``//``, + which is dropped if empty, but conveys different meanings in the + final URL if it's present or not. In this case, you can use + ``safe_conversion``, which will return the URL unchanged if it only + contains ASCII characters and no whitespace. This can result in a + URI with unquoted characters if it was not already quoted correctly, + but preserves the URL's semantics. Werkzeug uses this for the + ``Location`` header for redirects. + + .. versionchanged:: 0.15 + All reserved characters remain unquoted. Previously, only some + reserved characters were left unquoted. + + .. versionchanged:: 0.9.6 + The ``safe_conversion`` parameter was added. + + .. versionadded:: 0.6 + """ + if isinstance(iri, tuple): + iri = url_unparse(iri) + + if safe_conversion: + # If we're not sure if it's safe to convert the URL, and it only + # contains ASCII characters, return it unconverted. + try: + native_iri = _to_str(iri) + ascii_iri = native_iri.encode("ascii") + + # Only return if it doesn't have whitespace. (Why?) + if len(ascii_iri.split()) == 1: + return native_iri + except UnicodeError: + pass + + iri = url_parse(_to_str(iri, charset, errors)) + path = url_quote(iri.path, charset, errors, _to_uri_safe) + query = url_quote(iri.query, charset, errors, _to_uri_safe) + fragment = url_quote(iri.fragment, charset, errors, _to_uri_safe) + return url_unparse((iri.scheme, iri.encode_netloc(), path, query, fragment)) + + +def url_decode( + s: t.AnyStr, + charset: str = "utf-8", + decode_keys: None = None, + include_empty: bool = True, + errors: str = "replace", + separator: str = "&", + cls: t.Optional[t.Type["ds.MultiDict"]] = None, +) -> "ds.MultiDict[str, str]": + """Parse a query string and return it as a :class:`MultiDict`. + + :param s: The query string to parse. + :param charset: Decode bytes to string with this charset. If not + given, bytes are returned as-is. + :param include_empty: Include keys with empty values in the dict. + :param errors: Error handling behavior when decoding bytes. + :param separator: Separator character between pairs. + :param cls: Container to hold result instead of :class:`MultiDict`. + + .. versionchanged:: 2.0 + The ``decode_keys`` parameter is deprecated and will be removed + in Werkzeug 2.1. + + .. versionchanged:: 0.5 + In previous versions ";" and "&" could be used for url decoding. + Now only "&" is supported. If you want to use ";", a different + ``separator`` can be provided. + + .. versionchanged:: 0.5 + The ``cls`` parameter was added. + """ + if decode_keys is not None: + warnings.warn( + "'decode_keys' is deprecated and will be removed in Werkzeug 2.1.", + DeprecationWarning, + stacklevel=2, + ) + if cls is None: + from .datastructures import MultiDict # noqa: F811 + + cls = MultiDict + if isinstance(s, str) and not isinstance(separator, str): + separator = separator.decode(charset or "ascii") + elif isinstance(s, bytes) and not isinstance(separator, bytes): + separator = separator.encode(charset or "ascii") # type: ignore + return cls( + _url_decode_impl( + s.split(separator), charset, include_empty, errors # type: ignore + ) + ) + + +def url_decode_stream( + stream: t.IO[bytes], + charset: str = "utf-8", + decode_keys: None = None, + include_empty: bool = True, + errors: str = "replace", + separator: bytes = b"&", + cls: t.Optional[t.Type["ds.MultiDict"]] = None, + limit: t.Optional[int] = None, + return_iterator: bool = False, +) -> "ds.MultiDict[str, str]": + """Works like :func:`url_decode` but decodes a stream. The behavior + of stream and limit follows functions like + :func:`~werkzeug.wsgi.make_line_iter`. The generator of pairs is + directly fed to the `cls` so you can consume the data while it's + parsed. + + :param stream: a stream with the encoded querystring + :param charset: the charset of the query string. If set to `None` + no decoding will take place. + :param include_empty: Set to `False` if you don't want empty values to + appear in the dict. + :param errors: the decoding error behavior. + :param separator: the pair separator to be used, defaults to ``&`` + :param cls: an optional dict class to use. If this is not specified + or `None` the default :class:`MultiDict` is used. + :param limit: the content length of the URL data. Not necessary if + a limited stream is provided. + + .. versionchanged:: 2.0 + The ``decode_keys`` and ``return_iterator`` parameters are + deprecated and will be removed in Werkzeug 2.1. + + .. versionadded:: 0.8 + """ + from .wsgi import make_chunk_iter + + if decode_keys is not None: + warnings.warn( + "'decode_keys' is deprecated and will be removed in Werkzeug 2.1.", + DeprecationWarning, + stacklevel=2, + ) + + pair_iter = make_chunk_iter(stream, separator, limit) + decoder = _url_decode_impl(pair_iter, charset, include_empty, errors) + + if return_iterator: + warnings.warn( + "'return_iterator' is deprecated and will be removed in Werkzeug 2.1.", + DeprecationWarning, + stacklevel=2, + ) + return decoder # type: ignore + + if cls is None: + from .datastructures import MultiDict # noqa: F811 + + cls = MultiDict + + return cls(decoder) + + +def _url_decode_impl( + pair_iter: t.Iterable[t.AnyStr], charset: str, include_empty: bool, errors: str +) -> t.Iterator[t.Tuple[str, str]]: + for pair in pair_iter: + if not pair: + continue + s = _make_encode_wrapper(pair) + equal = s("=") + if equal in pair: + key, value = pair.split(equal, 1) + else: + if not include_empty: + continue + key = pair + value = s("") + yield ( + url_unquote_plus(key, charset, errors), + url_unquote_plus(value, charset, errors), + ) + + +def url_encode( + obj: t.Union[t.Mapping[str, str], t.Iterable[t.Tuple[str, str]]], + charset: str = "utf-8", + encode_keys: None = None, + sort: bool = False, + key: t.Optional[t.Callable[[t.Tuple[str, str]], t.Any]] = None, + separator: str = "&", +) -> str: + """URL encode a dict/`MultiDict`. If a value is `None` it will not appear + in the result string. Per default only values are encoded into the target + charset strings. + + :param obj: the object to encode into a query string. + :param charset: the charset of the query string. + :param sort: set to `True` if you want parameters to be sorted by `key`. + :param separator: the separator to be used for the pairs. + :param key: an optional function to be used for sorting. For more details + check out the :func:`sorted` documentation. + + .. versionchanged:: 2.0 + The ``encode_keys`` parameter is deprecated and will be removed + in Werkzeug 2.1. + + .. versionchanged:: 0.5 + Added the ``sort``, ``key``, and ``separator`` parameters. + """ + if encode_keys is not None: + warnings.warn( + "'encode_keys' is deprecated and will be removed in Werkzeug 2.1.", + DeprecationWarning, + stacklevel=2, + ) + separator = _to_str(separator, "ascii") + return separator.join(_url_encode_impl(obj, charset, sort, key)) + + +def url_encode_stream( + obj: t.Union[t.Mapping[str, str], t.Iterable[t.Tuple[str, str]]], + stream: t.Optional[t.IO[str]] = None, + charset: str = "utf-8", + encode_keys: None = None, + sort: bool = False, + key: t.Optional[t.Callable[[t.Tuple[str, str]], t.Any]] = None, + separator: str = "&", +) -> None: + """Like :meth:`url_encode` but writes the results to a stream + object. If the stream is `None` a generator over all encoded + pairs is returned. + + :param obj: the object to encode into a query string. + :param stream: a stream to write the encoded object into or `None` if + an iterator over the encoded pairs should be returned. In + that case the separator argument is ignored. + :param charset: the charset of the query string. + :param sort: set to `True` if you want parameters to be sorted by `key`. + :param separator: the separator to be used for the pairs. + :param key: an optional function to be used for sorting. For more details + check out the :func:`sorted` documentation. + + .. versionchanged:: 2.0 + The ``encode_keys`` parameter is deprecated and will be removed + in Werkzeug 2.1. + + .. versionadded:: 0.8 + """ + if encode_keys is not None: + warnings.warn( + "'encode_keys' is deprecated and will be removed in Werkzeug 2.1.", + DeprecationWarning, + stacklevel=2, + ) + separator = _to_str(separator, "ascii") + gen = _url_encode_impl(obj, charset, sort, key) + if stream is None: + return gen # type: ignore + for idx, chunk in enumerate(gen): + if idx: + stream.write(separator) + stream.write(chunk) + return None + + +def url_join( + base: t.Union[str, t.Tuple[str, str, str, str, str]], + url: t.Union[str, t.Tuple[str, str, str, str, str]], + allow_fragments: bool = True, +) -> str: + """Join a base URL and a possibly relative URL to form an absolute + interpretation of the latter. + + :param base: the base URL for the join operation. + :param url: the URL to join. + :param allow_fragments: indicates whether fragments should be allowed. + """ + if isinstance(base, tuple): + base = url_unparse(base) + if isinstance(url, tuple): + url = url_unparse(url) + + _check_str_tuple((base, url)) + s = _make_encode_wrapper(base) + + if not base: + return url + if not url: + return base + + bscheme, bnetloc, bpath, bquery, bfragment = url_parse( + base, allow_fragments=allow_fragments + ) + scheme, netloc, path, query, fragment = url_parse(url, bscheme, allow_fragments) + if scheme != bscheme: + return url + if netloc: + return url_unparse((scheme, netloc, path, query, fragment)) + netloc = bnetloc + + if path[:1] == s("/"): + segments = path.split(s("/")) + elif not path: + segments = bpath.split(s("/")) + if not query: + query = bquery + else: + segments = bpath.split(s("/"))[:-1] + path.split(s("/")) + + # If the rightmost part is "./" we want to keep the slash but + # remove the dot. + if segments[-1] == s("."): + segments[-1] = s("") + + # Resolve ".." and "." + segments = [segment for segment in segments if segment != s(".")] + while True: + i = 1 + n = len(segments) - 1 + while i < n: + if segments[i] == s("..") and segments[i - 1] not in (s(""), s("..")): + del segments[i - 1 : i + 1] + break + i += 1 + else: + break + + # Remove trailing ".." if the URL is absolute + unwanted_marker = [s(""), s("..")] + while segments[:2] == unwanted_marker: + del segments[1] + + path = s("/").join(segments) + return url_unparse((scheme, netloc, path, query, fragment)) + + +class Href: + """Implements a callable that constructs URLs with the given base. The + function can be called with any number of positional and keyword + arguments which than are used to assemble the URL. Works with URLs + and posix paths. + + Positional arguments are appended as individual segments to + the path of the URL: + + >>> href = Href('/foo') + >>> href('bar', 23) + '/foo/bar/23' + >>> href('foo', bar=23) + '/foo/foo?bar=23' + + If any of the arguments (positional or keyword) evaluates to `None` it + will be skipped. If no keyword arguments are given the last argument + can be a :class:`dict` or :class:`MultiDict` (or any other dict subclass), + otherwise the keyword arguments are used for the query parameters, cutting + off the first trailing underscore of the parameter name: + + >>> href(is_=42) + '/foo?is=42' + >>> href({'foo': 'bar'}) + '/foo?foo=bar' + + Combining of both methods is not allowed: + + >>> href({'foo': 'bar'}, bar=42) + Traceback (most recent call last): + ... + TypeError: keyword arguments and query-dicts can't be combined + + Accessing attributes on the href object creates a new href object with + the attribute name as prefix: + + >>> bar_href = href.bar + >>> bar_href("blub") + '/foo/bar/blub' + + If `sort` is set to `True` the items are sorted by `key` or the default + sorting algorithm: + + >>> href = Href("/", sort=True) + >>> href(a=1, b=2, c=3) + '/?a=1&b=2&c=3' + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Use :mod:`werkzeug.routing` + instead. + + .. versionadded:: 0.5 + `sort` and `key` were added. + """ + + def __init__( # type: ignore + self, base="./", charset="utf-8", sort=False, key=None + ): + warnings.warn( + "'Href' is deprecated and will be removed in Werkzeug 2.1." + " Use 'werkzeug.routing' instead.", + DeprecationWarning, + stacklevel=2, + ) + + if not base: + base = "./" + self.base = base + self.charset = charset + self.sort = sort + self.key = key + + def __getattr__(self, name): # type: ignore + if name[:2] == "__": + raise AttributeError(name) + base = self.base + if base[-1:] != "/": + base += "/" + return Href(url_join(base, name), self.charset, self.sort, self.key) + + def __call__(self, *path, **query): # type: ignore + if path and isinstance(path[-1], dict): + if query: + raise TypeError("keyword arguments and query-dicts can't be combined") + query, path = path[-1], path[:-1] + elif query: + query = {k[:-1] if k.endswith("_") else k: v for k, v in query.items()} + path = "/".join( + [ + _to_str(url_quote(x, self.charset), "ascii") + for x in path + if x is not None + ] + ).lstrip("/") + rv = self.base + if path: + if not rv.endswith("/"): + rv += "/" + rv = url_join(rv, f"./{path}") + if query: + rv += "?" + _to_str( + url_encode(query, self.charset, sort=self.sort, key=self.key), "ascii" + ) + return rv diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/user_agent.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/user_agent.py new file mode 100644 index 000000000..66ffcbe07 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/user_agent.py @@ -0,0 +1,47 @@ +import typing as t + + +class UserAgent: + """Represents a parsed user agent header value. + + The default implementation does no parsing, only the :attr:`string` + attribute is set. A subclass may parse the string to set the + common attributes or expose other information. Set + :attr:`werkzeug.wrappers.Request.user_agent_class` to use a + subclass. + + :param string: The header value to parse. + + .. versionadded:: 2.0 + This replaces the previous ``useragents`` module, but does not + provide a built-in parser. + """ + + platform: t.Optional[str] = None + """The OS name, if it could be parsed from the string.""" + + browser: t.Optional[str] = None + """The browser name, if it could be parsed from the string.""" + + version: t.Optional[str] = None + """The browser version, if it could be parsed from the string.""" + + language: t.Optional[str] = None + """The browser language, if it could be parsed from the string.""" + + def __init__(self, string: str) -> None: + self.string: str = string + """The original header value.""" + + def __repr__(self) -> str: + return f"<{type(self).__name__} {self.browser}/{self.version}>" + + def __str__(self) -> str: + return self.string + + def __bool__(self) -> bool: + return bool(self.browser) + + def to_header(self) -> str: + """Convert to a header value.""" + return self.string diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/useragents.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/useragents.py new file mode 100644 index 000000000..4deed8f46 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/useragents.py @@ -0,0 +1,215 @@ +import re +import typing as t +import warnings + +from .user_agent import UserAgent as _BaseUserAgent + +if t.TYPE_CHECKING: + from _typeshed.wsgi import WSGIEnvironment + + +class _UserAgentParser: + platform_rules: t.ClassVar[t.Iterable[t.Tuple[str, str]]] = ( + (" cros ", "chromeos"), + ("iphone|ios", "iphone"), + ("ipad", "ipad"), + (r"darwin\b|mac\b|os\s*x", "macos"), + ("win", "windows"), + (r"android", "android"), + ("netbsd", "netbsd"), + ("openbsd", "openbsd"), + ("freebsd", "freebsd"), + ("dragonfly", "dragonflybsd"), + ("(sun|i86)os", "solaris"), + (r"x11\b|lin(\b|ux)?", "linux"), + (r"nintendo\s+wii", "wii"), + ("irix", "irix"), + ("hp-?ux", "hpux"), + ("aix", "aix"), + ("sco|unix_sv", "sco"), + ("bsd", "bsd"), + ("amiga", "amiga"), + ("blackberry|playbook", "blackberry"), + ("symbian", "symbian"), + ) + browser_rules: t.ClassVar[t.Iterable[t.Tuple[str, str]]] = ( + ("googlebot", "google"), + ("msnbot", "msn"), + ("yahoo", "yahoo"), + ("ask jeeves", "ask"), + (r"aol|america\s+online\s+browser", "aol"), + (r"opera|opr", "opera"), + ("edge|edg", "edge"), + ("chrome|crios", "chrome"), + ("seamonkey", "seamonkey"), + ("firefox|firebird|phoenix|iceweasel", "firefox"), + ("galeon", "galeon"), + ("safari|version", "safari"), + ("webkit", "webkit"), + ("camino", "camino"), + ("konqueror", "konqueror"), + ("k-meleon", "kmeleon"), + ("netscape", "netscape"), + (r"msie|microsoft\s+internet\s+explorer|trident/.+? rv:", "msie"), + ("lynx", "lynx"), + ("links", "links"), + ("Baiduspider", "baidu"), + ("bingbot", "bing"), + ("mozilla", "mozilla"), + ) + + _browser_version_re = r"(?:{pattern})[/\sa-z(]*(\d+[.\da-z]+)?" + _language_re = re.compile( + r"(?:;\s*|\s+)(\b\w{2}\b(?:-\b\w{2}\b)?)\s*;|" + r"(?:\(|\[|;)\s*(\b\w{2}\b(?:-\b\w{2}\b)?)\s*(?:\]|\)|;)" + ) + + def __init__(self) -> None: + self.platforms = [(b, re.compile(a, re.I)) for a, b in self.platform_rules] + self.browsers = [ + (b, re.compile(self._browser_version_re.format(pattern=a), re.I)) + for a, b in self.browser_rules + ] + + def __call__( + self, user_agent: str + ) -> t.Tuple[t.Optional[str], t.Optional[str], t.Optional[str], t.Optional[str]]: + platform: t.Optional[str] + browser: t.Optional[str] + version: t.Optional[str] + language: t.Optional[str] + + for platform, regex in self.platforms: # noqa: B007 + match = regex.search(user_agent) + if match is not None: + break + else: + platform = None + + # Except for Trident, all browser key words come after the last ')' + last_closing_paren = 0 + if ( + not re.compile(r"trident/.+? rv:", re.I).search(user_agent) + and ")" in user_agent + and user_agent[-1] != ")" + ): + last_closing_paren = user_agent.rindex(")") + + for browser, regex in self.browsers: # noqa: B007 + match = regex.search(user_agent[last_closing_paren:]) + if match is not None: + version = match.group(1) + break + else: + browser = version = None + match = self._language_re.search(user_agent) + if match is not None: + language = match.group(1) or match.group(2) + else: + language = None + return platform, browser, version, language + + +# It wasn't public, but users might have imported it anyway, show a +# warning if a user created an instance. +class UserAgentParser(_UserAgentParser): + """A simple user agent parser. Used by the `UserAgent`. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Use a dedicated parser library + instead. + """ + + def __init__(self) -> None: + warnings.warn( + "'UserAgentParser' is deprecated and will be removed in" + " Werkzeug 2.1. Use a dedicated parser library instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__() + + +class _deprecated_property(property): + def __init__(self, fget: t.Callable[["_UserAgent"], t.Any]) -> None: + super().__init__(fget) + self.message = ( + "The built-in user agent parser is deprecated and will be" + f" removed in Werkzeug 2.1. The {fget.__name__!r} property" + " will be 'None'. Subclass 'werkzeug.user_agent.UserAgent'" + " and set 'Request.user_agent_class' to use a different" + " parser." + ) + + def __get__(self, *args: t.Any, **kwargs: t.Any) -> t.Any: + warnings.warn(self.message, DeprecationWarning, stacklevel=3) + return super().__get__(*args, **kwargs) + + +# This is what Request.user_agent returns for now, only show warnings on +# attribute access, not creation. +class _UserAgent(_BaseUserAgent): + _parser = _UserAgentParser() + + def __init__(self, string: str) -> None: + super().__init__(string) + info = self._parser(string) + self._platform, self._browser, self._version, self._language = info + + @_deprecated_property + def platform(self) -> t.Optional[str]: # type: ignore + return self._platform + + @_deprecated_property + def browser(self) -> t.Optional[str]: # type: ignore + return self._browser + + @_deprecated_property + def version(self) -> t.Optional[str]: # type: ignore + return self._version + + @_deprecated_property + def language(self) -> t.Optional[str]: # type: ignore + return self._language + + +# This is what users might be importing, show warnings on create. +class UserAgent(_UserAgent): + """Represents a parsed user agent header value. + + This uses a basic parser to try to extract some information from the + header. + + :param environ_or_string: The header value to parse, or a WSGI + environ containing the header. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Subclass + :class:`werkzeug.user_agent.UserAgent` (note the new module + name) to use a dedicated parser instead. + + .. versionchanged:: 2.0 + Passing a WSGI environ is deprecated and will be removed in 2.1. + """ + + def __init__(self, environ_or_string: "t.Union[str, WSGIEnvironment]") -> None: + if isinstance(environ_or_string, dict): + warnings.warn( + "Passing an environ to 'UserAgent' is deprecated and" + " will be removed in Werkzeug 2.1. Pass the header" + " value string instead.", + DeprecationWarning, + stacklevel=2, + ) + string = environ_or_string.get("HTTP_USER_AGENT", "") + else: + string = environ_or_string + + warnings.warn( + "The 'werkzeug.useragents' module is deprecated and will be" + " removed in Werkzeug 2.1. The new base API is" + " 'werkzeug.user_agent.UserAgent'.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(string) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/utils.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/utils.py new file mode 100644 index 000000000..900723149 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/utils.py @@ -0,0 +1,1099 @@ +import codecs +import io +import mimetypes +import os +import pkgutil +import re +import sys +import typing as t +import unicodedata +import warnings +from datetime import datetime +from html.entities import name2codepoint +from time import time +from zlib import adler32 + +from ._internal import _DictAccessorProperty +from ._internal import _missing +from ._internal import _parse_signature +from ._internal import _TAccessorValue +from .datastructures import Headers +from .exceptions import NotFound +from .exceptions import RequestedRangeNotSatisfiable +from .security import safe_join +from .urls import url_quote +from .wsgi import wrap_file + +if t.TYPE_CHECKING: + from _typeshed.wsgi import WSGIEnvironment + from .wrappers.request import Request + from .wrappers.response import Response + +_T = t.TypeVar("_T") + +_entity_re = re.compile(r"&([^;]+);") +_filename_ascii_strip_re = re.compile(r"[^A-Za-z0-9_.-]") +_windows_device_files = ( + "CON", + "AUX", + "COM1", + "COM2", + "COM3", + "COM4", + "LPT1", + "LPT2", + "LPT3", + "PRN", + "NUL", +) + + +class cached_property(property, t.Generic[_T]): + """A :func:`property` that is only evaluated once. Subsequent access + returns the cached value. Setting the property sets the cached + value. Deleting the property clears the cached value, accessing it + again will evaluate it again. + + .. code-block:: python + + class Example: + @cached_property + def value(self): + # calculate something important here + return 42 + + e = Example() + e.value # evaluates + e.value # uses cache + e.value = 16 # sets cache + del e.value # clears cache + + The class must have a ``__dict__`` for this to work. + + .. versionchanged:: 2.0 + ``del obj.name`` clears the cached value. + """ + + def __init__( + self, + fget: t.Callable[[t.Any], _T], + name: t.Optional[str] = None, + doc: t.Optional[str] = None, + ) -> None: + super().__init__(fget, doc=doc) + self.__name__ = name or fget.__name__ + self.__module__ = fget.__module__ + + def __set__(self, obj: object, value: _T) -> None: + obj.__dict__[self.__name__] = value + + def __get__(self, obj: object, type: type = None) -> _T: # type: ignore + if obj is None: + return self # type: ignore + + value: _T = obj.__dict__.get(self.__name__, _missing) + + if value is _missing: + value = self.fget(obj) # type: ignore + obj.__dict__[self.__name__] = value + + return value + + def __delete__(self, obj: object) -> None: + del obj.__dict__[self.__name__] + + +def invalidate_cached_property(obj: object, name: str) -> None: + """Invalidates the cache for a :class:`cached_property`: + + >>> class Test(object): + ... @cached_property + ... def magic_number(self): + ... print("recalculating...") + ... return 42 + ... + >>> var = Test() + >>> var.magic_number + recalculating... + 42 + >>> var.magic_number + 42 + >>> invalidate_cached_property(var, "magic_number") + >>> var.magic_number + recalculating... + 42 + + You must pass the name of the cached property as the second argument. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Use ``del obj.name`` instead. + """ + warnings.warn( + "'invalidate_cached_property' is deprecated and will be removed" + " in Werkzeug 2.1. Use 'del obj.name' instead.", + DeprecationWarning, + stacklevel=2, + ) + delattr(obj, name) + + +class environ_property(_DictAccessorProperty[_TAccessorValue]): + """Maps request attributes to environment variables. This works not only + for the Werkzeug request object, but also any other class with an + environ attribute: + + >>> class Test(object): + ... environ = {'key': 'value'} + ... test = environ_property('key') + >>> var = Test() + >>> var.test + 'value' + + If you pass it a second value it's used as default if the key does not + exist, the third one can be a converter that takes a value and converts + it. If it raises :exc:`ValueError` or :exc:`TypeError` the default value + is used. If no default value is provided `None` is used. + + Per default the property is read only. You have to explicitly enable it + by passing ``read_only=False`` to the constructor. + """ + + read_only = True + + def lookup(self, obj: "Request") -> "WSGIEnvironment": + return obj.environ + + +class header_property(_DictAccessorProperty[_TAccessorValue]): + """Like `environ_property` but for headers.""" + + def lookup(self, obj: t.Union["Request", "Response"]) -> Headers: + return obj.headers + + +class HTMLBuilder: + """Helper object for HTML generation. + + Per default there are two instances of that class. The `html` one, and + the `xhtml` one for those two dialects. The class uses keyword parameters + and positional parameters to generate small snippets of HTML. + + Keyword parameters are converted to XML/SGML attributes, positional + arguments are used as children. Because Python accepts positional + arguments before keyword arguments it's a good idea to use a list with the + star-syntax for some children: + + >>> html.p(class_='foo', *[html.a('foo', href='foo.html'), ' ', + ... html.a('bar', href='bar.html')]) + '

    foo bar

    ' + + This class works around some browser limitations and can not be used for + arbitrary SGML/XML generation. For that purpose lxml and similar + libraries exist. + + Calling the builder escapes the string passed: + + >>> html.p(html("")) + '

    <foo>

    ' + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. + """ + + _entity_re = re.compile(r"&([^;]+);") + _entities = name2codepoint.copy() + _entities["apos"] = 39 + _empty_elements = { + "area", + "base", + "basefont", + "br", + "col", + "command", + "embed", + "frame", + "hr", + "img", + "input", + "keygen", + "isindex", + "link", + "meta", + "param", + "source", + "wbr", + } + _boolean_attributes = { + "selected", + "checked", + "compact", + "declare", + "defer", + "disabled", + "ismap", + "multiple", + "nohref", + "noresize", + "noshade", + "nowrap", + } + _plaintext_elements = {"textarea"} + _c_like_cdata = {"script", "style"} + + def __init__(self, dialect): # type: ignore + self._dialect = dialect + + def __call__(self, s): # type: ignore + import html + + warnings.warn( + "'utils.HTMLBuilder' is deprecated and will be removed in Werkzeug 2.1.", + DeprecationWarning, + stacklevel=2, + ) + return html.escape(s) + + def __getattr__(self, tag): # type: ignore + import html + + warnings.warn( + "'utils.HTMLBuilder' is deprecated and will be removed in Werkzeug 2.1.", + DeprecationWarning, + stacklevel=2, + ) + if tag[:2] == "__": + raise AttributeError(tag) + + def proxy(*children, **arguments): # type: ignore + buffer = f"<{tag}" + for key, value in arguments.items(): + if value is None: + continue + if key[-1] == "_": + key = key[:-1] + if key in self._boolean_attributes: + if not value: + continue + if self._dialect == "xhtml": + value = f'="{key}"' + else: + value = "" + else: + value = f'="{html.escape(value)}"' + buffer += f" {key}{value}" + if not children and tag in self._empty_elements: + if self._dialect == "xhtml": + buffer += " />" + else: + buffer += ">" + return buffer + buffer += ">" + + children_as_string = "".join([str(x) for x in children if x is not None]) + + if children_as_string: + if tag in self._plaintext_elements: + children_as_string = html.escape(children_as_string) + elif tag in self._c_like_cdata and self._dialect == "xhtml": + children_as_string = f"/**/" + buffer += children_as_string + f"" + return buffer + + return proxy + + def __repr__(self) -> str: + return f"<{type(self).__name__} for {self._dialect!r}>" + + +html = HTMLBuilder("html") +xhtml = HTMLBuilder("xhtml") + +# https://cgit.freedesktop.org/xdg/shared-mime-info/tree/freedesktop.org.xml.in +# https://www.iana.org/assignments/media-types/media-types.xhtml +# Types listed in the XDG mime info that have a charset in the IANA registration. +_charset_mimetypes = { + "application/ecmascript", + "application/javascript", + "application/sql", + "application/xml", + "application/xml-dtd", + "application/xml-external-parsed-entity", +} + + +def get_content_type(mimetype: str, charset: str) -> str: + """Returns the full content type string with charset for a mimetype. + + If the mimetype represents text, the charset parameter will be + appended, otherwise the mimetype is returned unchanged. + + :param mimetype: The mimetype to be used as content type. + :param charset: The charset to be appended for text mimetypes. + :return: The content type. + + .. versionchanged:: 0.15 + Any type that ends with ``+xml`` gets a charset, not just those + that start with ``application/``. Known text types such as + ``application/javascript`` are also given charsets. + """ + if ( + mimetype.startswith("text/") + or mimetype in _charset_mimetypes + or mimetype.endswith("+xml") + ): + mimetype += f"; charset={charset}" + + return mimetype + + +def detect_utf_encoding(data: bytes) -> str: + """Detect which UTF encoding was used to encode the given bytes. + + The latest JSON standard (:rfc:`8259`) suggests that only UTF-8 is + accepted. Older documents allowed 8, 16, or 32. 16 and 32 can be big + or little endian. Some editors or libraries may prepend a BOM. + + :internal: + + :param data: Bytes in unknown UTF encoding. + :return: UTF encoding name + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. This is built in to + :func:`json.loads`. + + .. versionadded:: 0.15 + """ + warnings.warn( + "'detect_utf_encoding' is deprecated and will be removed in" + " Werkzeug 2.1. This is built in to 'json.loads'.", + DeprecationWarning, + stacklevel=2, + ) + head = data[:4] + + if head[:3] == codecs.BOM_UTF8: + return "utf-8-sig" + + if b"\x00" not in head: + return "utf-8" + + if head in (codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE): + return "utf-32" + + if head[:2] in (codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE): + return "utf-16" + + if len(head) == 4: + if head[:3] == b"\x00\x00\x00": + return "utf-32-be" + + if head[::2] == b"\x00\x00": + return "utf-16-be" + + if head[1:] == b"\x00\x00\x00": + return "utf-32-le" + + if head[1::2] == b"\x00\x00": + return "utf-16-le" + + if len(head) == 2: + return "utf-16-be" if head.startswith(b"\x00") else "utf-16-le" + + return "utf-8" + + +def format_string(string: str, context: t.Mapping[str, t.Any]) -> str: + """String-template format a string: + + >>> format_string('$foo and ${foo}s', dict(foo=42)) + '42 and 42s' + + This does not do any attribute lookup. + + :param string: the format string. + :param context: a dict with the variables to insert. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Use :class:`string.Template` + instead. + """ + from string import Template + + warnings.warn( + "'utils.format_string' is deprecated and will be removed in" + " Werkzeug 2.1. Use 'string.Template' instead.", + DeprecationWarning, + stacklevel=2, + ) + return Template(string).substitute(context) + + +def secure_filename(filename: str) -> str: + r"""Pass it a filename and it will return a secure version of it. This + filename can then safely be stored on a regular file system and passed + to :func:`os.path.join`. The filename returned is an ASCII only string + for maximum portability. + + On windows systems the function also makes sure that the file is not + named after one of the special device files. + + >>> secure_filename("My cool movie.mov") + 'My_cool_movie.mov' + >>> secure_filename("../../../etc/passwd") + 'etc_passwd' + >>> secure_filename('i contain cool \xfcml\xe4uts.txt') + 'i_contain_cool_umlauts.txt' + + The function might return an empty filename. It's your responsibility + to ensure that the filename is unique and that you abort or + generate a random filename if the function returned an empty one. + + .. versionadded:: 0.5 + + :param filename: the filename to secure + """ + filename = unicodedata.normalize("NFKD", filename) + filename = filename.encode("ascii", "ignore").decode("ascii") + + for sep in os.path.sep, os.path.altsep: + if sep: + filename = filename.replace(sep, " ") + filename = str(_filename_ascii_strip_re.sub("", "_".join(filename.split()))).strip( + "._" + ) + + # on nt a couple of special files are present in each folder. We + # have to ensure that the target file is not such a filename. In + # this case we prepend an underline + if ( + os.name == "nt" + and filename + and filename.split(".")[0].upper() in _windows_device_files + ): + filename = f"_{filename}" + + return filename + + +def escape(s: t.Any) -> str: + """Replace ``&``, ``<``, ``>``, ``"``, and ``'`` with HTML-safe + sequences. + + ``None`` is escaped to an empty string. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Use MarkupSafe instead. + """ + import html + + warnings.warn( + "'utils.escape' is deprecated and will be removed in Werkzeug" + " 2.1. Use MarkupSafe instead.", + DeprecationWarning, + stacklevel=2, + ) + + if s is None: + return "" + + if hasattr(s, "__html__"): + return s.__html__() # type: ignore + + if not isinstance(s, str): + s = str(s) + + return html.escape(s, quote=True) # type: ignore + + +def unescape(s: str) -> str: + """The reverse of :func:`escape`. This unescapes all the HTML + entities, not only those inserted by ``escape``. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Use MarkupSafe instead. + """ + import html + + warnings.warn( + "'utils.unescape' is deprecated and will be removed in Werkzueg" + " 2.1. Use MarkupSafe instead.", + DeprecationWarning, + stacklevel=2, + ) + return html.unescape(s) + + +def redirect( + location: str, code: int = 302, Response: t.Optional[t.Type["Response"]] = None +) -> "Response": + """Returns a response object (a WSGI application) that, if called, + redirects the client to the target location. Supported codes are + 301, 302, 303, 305, 307, and 308. 300 is not supported because + it's not a real redirect and 304 because it's the answer for a + request with a request with defined If-Modified-Since headers. + + .. versionadded:: 0.6 + The location can now be a unicode string that is encoded using + the :func:`iri_to_uri` function. + + .. versionadded:: 0.10 + The class used for the Response object can now be passed in. + + :param location: the location the response should redirect to. + :param code: the redirect status code. defaults to 302. + :param class Response: a Response class to use when instantiating a + response. The default is :class:`werkzeug.wrappers.Response` if + unspecified. + """ + import html + + if Response is None: + from .wrappers import Response # type: ignore + + display_location = html.escape(location) + if isinstance(location, str): + # Safe conversion is necessary here as we might redirect + # to a broken URI scheme (for instance itms-services). + from .urls import iri_to_uri + + location = iri_to_uri(location, safe_conversion=True) + response = Response( # type: ignore + '\n' + "Redirecting...\n" + "

    Redirecting...

    \n" + "

    You should be redirected automatically to target URL: " + f'{display_location}. If' + " not click the link.", + code, + mimetype="text/html", + ) + response.headers["Location"] = location + return response + + +def append_slash_redirect(environ: "WSGIEnvironment", code: int = 301) -> "Response": + """Redirects to the same URL but with a slash appended. The behavior + of this function is undefined if the path ends with a slash already. + + :param environ: the WSGI environment for the request that triggers + the redirect. + :param code: the status code for the redirect. + """ + new_path = environ["PATH_INFO"].strip("/") + "/" + query_string = environ.get("QUERY_STRING") + if query_string: + new_path += f"?{query_string}" + return redirect(new_path, code) + + +def send_file( + path_or_file: t.Union[os.PathLike, str, t.IO[bytes]], + environ: "WSGIEnvironment", + mimetype: t.Optional[str] = None, + as_attachment: bool = False, + download_name: t.Optional[str] = None, + conditional: bool = True, + etag: t.Union[bool, str] = True, + last_modified: t.Optional[t.Union[datetime, int, float]] = None, + max_age: t.Optional[ + t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]] + ] = None, + use_x_sendfile: bool = False, + response_class: t.Optional[t.Type["Response"]] = None, + _root_path: t.Optional[t.Union[os.PathLike, str]] = None, +) -> "Response": + """Send the contents of a file to the client. + + The first argument can be a file path or a file-like object. Paths + are preferred in most cases because Werkzeug can manage the file and + get extra information from the path. Passing a file-like object + requires that the file is opened in binary mode, and is mostly + useful when building a file in memory with :class:`io.BytesIO`. + + Never pass file paths provided by a user. The path is assumed to be + trusted, so a user could craft a path to access a file you didn't + intend. + + If the WSGI server sets a ``file_wrapper`` in ``environ``, it is + used, otherwise Werkzeug's built-in wrapper is used. Alternatively, + if the HTTP server supports ``X-Sendfile``, ``use_x_sendfile=True`` + will tell the server to send the given path, which is much more + efficient than reading it in Python. + + :param path_or_file: The path to the file to send, relative to the + current working directory if a relative path is given. + Alternatively, a file-like object opened in binary mode. Make + sure the file pointer is seeked to the start of the data. + :param environ: The WSGI environ for the current request. + :param mimetype: The MIME type to send for the file. If not + provided, it will try to detect it from the file name. + :param as_attachment: Indicate to a browser that it should offer to + save the file instead of displaying it. + :param download_name: The default name browsers will use when saving + the file. Defaults to the passed file name. + :param conditional: Enable conditional and range responses based on + request headers. Requires passing a file path and ``environ``. + :param etag: Calculate an ETag for the file, which requires passing + a file path. Can also be a string to use instead. + :param last_modified: The last modified time to send for the file, + in seconds. If not provided, it will try to detect it from the + file path. + :param max_age: How long the client should cache the file, in + seconds. If set, ``Cache-Control`` will be ``public``, otherwise + it will be ``no-cache`` to prefer conditional caching. + :param use_x_sendfile: Set the ``X-Sendfile`` header to let the + server to efficiently send the file. Requires support from the + HTTP server. Requires passing a file path. + :param response_class: Build the response using this class. Defaults + to :class:`~werkzeug.wrappers.Response`. + :param _root_path: Do not use. For internal use only. Use + :func:`send_from_directory` to safely send files under a path. + + .. versionchanged:: 2.0.2 + ``send_file`` only sets a detected ``Content-Encoding`` if + ``as_attachment`` is disabled. + + .. versionadded:: 2.0 + Adapted from Flask's implementation. + + .. versionchanged:: 2.0 + ``download_name`` replaces Flask's ``attachment_filename`` + parameter. If ``as_attachment=False``, it is passed with + ``Content-Disposition: inline`` instead. + + .. versionchanged:: 2.0 + ``max_age`` replaces Flask's ``cache_timeout`` parameter. + ``conditional`` is enabled and ``max_age`` is not set by + default. + + .. versionchanged:: 2.0 + ``etag`` replaces Flask's ``add_etags`` parameter. It can be a + string to use instead of generating one. + + .. versionchanged:: 2.0 + If an encoding is returned when guessing ``mimetype`` from + ``download_name``, set the ``Content-Encoding`` header. + """ + if response_class is None: + from .wrappers import Response + + response_class = Response + + path: t.Optional[str] = None + file: t.Optional[t.IO[bytes]] = None + size: t.Optional[int] = None + mtime: t.Optional[float] = None + headers = Headers() + + if isinstance(path_or_file, (os.PathLike, str)) or hasattr( + path_or_file, "__fspath__" + ): + path_or_file = t.cast(t.Union[os.PathLike, str], path_or_file) + + # Flask will pass app.root_path, allowing its send_file wrapper + # to not have to deal with paths. + if _root_path is not None: + path = os.path.join(_root_path, path_or_file) + else: + path = os.path.abspath(path_or_file) + + stat = os.stat(path) + size = stat.st_size + mtime = stat.st_mtime + else: + file = path_or_file + + if download_name is None and path is not None: + download_name = os.path.basename(path) + + if mimetype is None: + if download_name is None: + raise TypeError( + "Unable to detect the MIME type because a file name is" + " not available. Either set 'download_name', pass a" + " path instead of a file, or set 'mimetype'." + ) + + mimetype, encoding = mimetypes.guess_type(download_name) + + if mimetype is None: + mimetype = "application/octet-stream" + + # Don't send encoding for attachments, it causes browsers to + # save decompress tar.gz files. + if encoding is not None and not as_attachment: + headers.set("Content-Encoding", encoding) + + if download_name is not None: + try: + download_name.encode("ascii") + except UnicodeEncodeError: + simple = unicodedata.normalize("NFKD", download_name) + simple = simple.encode("ascii", "ignore").decode("ascii") + quoted = url_quote(download_name, safe="") + names = {"filename": simple, "filename*": f"UTF-8''{quoted}"} + else: + names = {"filename": download_name} + + value = "attachment" if as_attachment else "inline" + headers.set("Content-Disposition", value, **names) + elif as_attachment: + raise TypeError( + "No name provided for attachment. Either set" + " 'download_name' or pass a path instead of a file." + ) + + if use_x_sendfile and path is not None: + headers["X-Sendfile"] = path + data = None + else: + if file is None: + file = open(path, "rb") # type: ignore + elif isinstance(file, io.BytesIO): + size = file.getbuffer().nbytes + elif isinstance(file, io.TextIOBase): + raise ValueError("Files must be opened in binary mode or use BytesIO.") + + data = wrap_file(environ, file) + + rv = response_class( + data, mimetype=mimetype, headers=headers, direct_passthrough=True + ) + + if size is not None: + rv.content_length = size + + if last_modified is not None: + rv.last_modified = last_modified # type: ignore + elif mtime is not None: + rv.last_modified = mtime # type: ignore + + rv.cache_control.no_cache = True + + # Flask will pass app.get_send_file_max_age, allowing its send_file + # wrapper to not have to deal with paths. + if callable(max_age): + max_age = max_age(path) + + if max_age is not None: + if max_age > 0: + rv.cache_control.no_cache = None + rv.cache_control.public = True + + rv.cache_control.max_age = max_age + rv.expires = int(time() + max_age) # type: ignore + + if isinstance(etag, str): + rv.set_etag(etag) + elif etag and path is not None: + check = adler32(path.encode("utf-8")) & 0xFFFFFFFF + rv.set_etag(f"{mtime}-{size}-{check}") + + if conditional: + try: + rv = rv.make_conditional(environ, accept_ranges=True, complete_length=size) + except RequestedRangeNotSatisfiable: + if file is not None: + file.close() + + raise + + # Some x-sendfile implementations incorrectly ignore the 304 + # status code and send the file anyway. + if rv.status_code == 304: + rv.headers.pop("x-sendfile", None) + + return rv + + +def send_from_directory( + directory: t.Union[os.PathLike, str], + path: t.Union[os.PathLike, str], + environ: "WSGIEnvironment", + **kwargs: t.Any, +) -> "Response": + """Send a file from within a directory using :func:`send_file`. + + This is a secure way to serve files from a folder, such as static + files or uploads. Uses :func:`~werkzeug.security.safe_join` to + ensure the path coming from the client is not maliciously crafted to + point outside the specified directory. + + If the final path does not point to an existing regular file, + returns a 404 :exc:`~werkzeug.exceptions.NotFound` error. + + :param directory: The directory that ``path`` must be located under. + :param path: The path to the file to send, relative to + ``directory``. + :param environ: The WSGI environ for the current request. + :param kwargs: Arguments to pass to :func:`send_file`. + + .. versionadded:: 2.0 + Adapted from Flask's implementation. + """ + path = safe_join(os.fspath(directory), os.fspath(path)) + + if path is None: + raise NotFound() + + # Flask will pass app.root_path, allowing its send_from_directory + # wrapper to not have to deal with paths. + if "_root_path" in kwargs: + path = os.path.join(kwargs["_root_path"], path) + + try: + if not os.path.isfile(path): + raise NotFound() + except ValueError: + # path contains null byte on Python < 3.8 + raise NotFound() from None + + return send_file(path, environ, **kwargs) + + +def import_string(import_name: str, silent: bool = False) -> t.Any: + """Imports an object based on a string. This is useful if you want to + use import paths as endpoints or something similar. An import path can + be specified either in dotted notation (``xml.sax.saxutils.escape``) + or with a colon as object delimiter (``xml.sax.saxutils:escape``). + + If `silent` is True the return value will be `None` if the import fails. + + :param import_name: the dotted name for the object to import. + :param silent: if set to `True` import errors are ignored and + `None` is returned instead. + :return: imported object + """ + import_name = import_name.replace(":", ".") + try: + try: + __import__(import_name) + except ImportError: + if "." not in import_name: + raise + else: + return sys.modules[import_name] + + module_name, obj_name = import_name.rsplit(".", 1) + module = __import__(module_name, globals(), locals(), [obj_name]) + try: + return getattr(module, obj_name) + except AttributeError as e: + raise ImportError(e) from None + + except ImportError as e: + if not silent: + raise ImportStringError(import_name, e).with_traceback( + sys.exc_info()[2] + ) from None + + return None + + +def find_modules( + import_path: str, include_packages: bool = False, recursive: bool = False +) -> t.Iterator[str]: + """Finds all the modules below a package. This can be useful to + automatically import all views / controllers so that their metaclasses / + function decorators have a chance to register themselves on the + application. + + Packages are not returned unless `include_packages` is `True`. This can + also recursively list modules but in that case it will import all the + packages to get the correct load path of that module. + + :param import_path: the dotted name for the package to find child modules. + :param include_packages: set to `True` if packages should be returned, too. + :param recursive: set to `True` if recursion should happen. + :return: generator + """ + module = import_string(import_path) + path = getattr(module, "__path__", None) + if path is None: + raise ValueError(f"{import_path!r} is not a package") + basename = f"{module.__name__}." + for _importer, modname, ispkg in pkgutil.iter_modules(path): + modname = basename + modname + if ispkg: + if include_packages: + yield modname + if recursive: + yield from find_modules(modname, include_packages, True) + else: + yield modname + + +def validate_arguments(func, args, kwargs, drop_extra=True): # type: ignore + """Checks if the function accepts the arguments and keyword arguments. + Returns a new ``(args, kwargs)`` tuple that can safely be passed to + the function without causing a `TypeError` because the function signature + is incompatible. If `drop_extra` is set to `True` (which is the default) + any extra positional or keyword arguments are dropped automatically. + + The exception raised provides three attributes: + + `missing` + A set of argument names that the function expected but where + missing. + + `extra` + A dict of keyword arguments that the function can not handle but + where provided. + + `extra_positional` + A list of values that where given by positional argument but the + function cannot accept. + + This can be useful for decorators that forward user submitted data to + a view function:: + + from werkzeug.utils import ArgumentValidationError, validate_arguments + + def sanitize(f): + def proxy(request): + data = request.values.to_dict() + try: + args, kwargs = validate_arguments(f, (request,), data) + except ArgumentValidationError: + raise BadRequest('The browser failed to transmit all ' + 'the data expected.') + return f(*args, **kwargs) + return proxy + + :param func: the function the validation is performed against. + :param args: a tuple of positional arguments. + :param kwargs: a dict of keyword arguments. + :param drop_extra: set to `False` if you don't want extra arguments + to be silently dropped. + :return: tuple in the form ``(args, kwargs)``. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Use :func:`inspect.signature` + instead. + """ + warnings.warn( + "'utils.validate_arguments' is deprecated and will be removed" + " in Werkzeug 2.1. Use 'inspect.signature' instead.", + DeprecationWarning, + stacklevel=2, + ) + parser = _parse_signature(func) + args, kwargs, missing, extra, extra_positional = parser(args, kwargs)[:5] + if missing: + raise ArgumentValidationError(tuple(missing)) + elif (extra or extra_positional) and not drop_extra: + raise ArgumentValidationError(None, extra, extra_positional) + return tuple(args), kwargs + + +def bind_arguments(func, args, kwargs): # type: ignore + """Bind the arguments provided into a dict. When passed a function, + a tuple of arguments and a dict of keyword arguments `bind_arguments` + returns a dict of names as the function would see it. This can be useful + to implement a cache decorator that uses the function arguments to build + the cache key based on the values of the arguments. + + :param func: the function the arguments should be bound for. + :param args: tuple of positional arguments. + :param kwargs: a dict of keyword arguments. + :return: a :class:`dict` of bound keyword arguments. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Use :meth:`Signature.bind` + instead. + """ + warnings.warn( + "'utils.bind_arguments' is deprecated and will be removed in" + " Werkzeug 2.1. Use 'Signature.bind' instead.", + DeprecationWarning, + stacklevel=2, + ) + ( + args, + kwargs, + missing, + extra, + extra_positional, + arg_spec, + vararg_var, + kwarg_var, + ) = _parse_signature(func)(args, kwargs) + values = {} + for (name, _has_default, _default), value in zip(arg_spec, args): + values[name] = value + if vararg_var is not None: + values[vararg_var] = tuple(extra_positional) + elif extra_positional: + raise TypeError("too many positional arguments") + if kwarg_var is not None: + multikw = set(extra) & {x[0] for x in arg_spec} + if multikw: + raise TypeError( + f"got multiple values for keyword argument {next(iter(multikw))!r}" + ) + values[kwarg_var] = extra + elif extra: + raise TypeError(f"got unexpected keyword argument {next(iter(extra))!r}") + return values + + +class ArgumentValidationError(ValueError): + """Raised if :func:`validate_arguments` fails to validate + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1 along with ``utils.bind`` and + ``validate_arguments``. + """ + + def __init__(self, missing=None, extra=None, extra_positional=None): # type: ignore + self.missing = set(missing or ()) + self.extra = extra or {} + self.extra_positional = extra_positional or [] + super().__init__( + "function arguments invalid." + f" ({len(self.missing)} missing," + f" {len(self.extra) + len(self.extra_positional)} additional)" + ) + + +class ImportStringError(ImportError): + """Provides information about a failed :func:`import_string` attempt.""" + + #: String in dotted notation that failed to be imported. + import_name: str + #: Wrapped exception. + exception: BaseException + + def __init__(self, import_name: str, exception: BaseException) -> None: + self.import_name = import_name + self.exception = exception + msg = import_name + name = "" + tracked = [] + for part in import_name.replace(":", ".").split("."): + name = f"{name}.{part}" if name else part + imported = import_string(name, silent=True) + if imported: + tracked.append((name, getattr(imported, "__file__", None))) + else: + track = [f"- {n!r} found in {i!r}." for n, i in tracked] + track.append(f"- {name!r} not found.") + track_str = "\n".join(track) + msg = ( + f"import_string() failed for {import_name!r}. Possible reasons" + f" are:\n\n" + "- missing __init__.py in a package;\n" + "- package or module path not included in sys.path;\n" + "- duplicated package or module name taking precedence in" + " sys.path;\n" + "- missing module, class, function or variable;\n\n" + f"Debugged import:\n\n{track_str}\n\n" + f"Original exception:\n\n{type(exception).__name__}: {exception}" + ) + break + + super().__init__(msg) + + def __repr__(self) -> str: + return f"<{type(self).__name__}({self.import_name!r}, {self.exception!r})>" diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__init__.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__init__.py new file mode 100644 index 000000000..eb69a9949 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__init__.py @@ -0,0 +1,16 @@ +from .accept import AcceptMixin +from .auth import AuthorizationMixin +from .auth import WWWAuthenticateMixin +from .base_request import BaseRequest +from .base_response import BaseResponse +from .common_descriptors import CommonRequestDescriptorsMixin +from .common_descriptors import CommonResponseDescriptorsMixin +from .etag import ETagRequestMixin +from .etag import ETagResponseMixin +from .request import PlainRequest +from .request import Request as Request +from .request import StreamOnlyMixin +from .response import Response as Response +from .response import ResponseStream +from .response import ResponseStreamMixin +from .user_agent import UserAgentMixin diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/__init__.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 000000000..63f9c8e29 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/__init__.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/accept.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/accept.cpython-38.pyc new file mode 100644 index 000000000..8e55b93be Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/accept.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/auth.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/auth.cpython-38.pyc new file mode 100644 index 000000000..be995c6cf Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/auth.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/base_request.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/base_request.cpython-38.pyc new file mode 100644 index 000000000..341bc4076 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/base_request.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/base_response.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/base_response.cpython-38.pyc new file mode 100644 index 000000000..54d19757d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/base_response.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/common_descriptors.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/common_descriptors.cpython-38.pyc new file mode 100644 index 000000000..22f6bf047 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/common_descriptors.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/cors.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/cors.cpython-38.pyc new file mode 100644 index 000000000..f3edd387f Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/cors.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/etag.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/etag.cpython-38.pyc new file mode 100644 index 000000000..ec1d1078e Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/etag.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/json.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/json.cpython-38.pyc new file mode 100644 index 000000000..4ff63e3eb Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/json.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/request.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/request.cpython-38.pyc new file mode 100644 index 000000000..82dac8d8b Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/request.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/response.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/response.cpython-38.pyc new file mode 100644 index 000000000..c879ca176 Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/response.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/user_agent.cpython-38.pyc b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/user_agent.cpython-38.pyc new file mode 100644 index 000000000..0831a954d Binary files /dev/null and b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/__pycache__/user_agent.cpython-38.pyc differ diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/accept.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/accept.py new file mode 100644 index 000000000..9605e637d --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/accept.py @@ -0,0 +1,14 @@ +import typing as t +import warnings + + +class AcceptMixin: + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'AcceptMixin' is deprecated and will be removed in" + " Werkzeug 2.1. 'Request' now includes the functionality" + " directly.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) # type: ignore diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/auth.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/auth.py new file mode 100644 index 000000000..da31b7cf7 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/auth.py @@ -0,0 +1,26 @@ +import typing as t +import warnings + + +class AuthorizationMixin: + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'AuthorizationMixin' is deprecated and will be removed in" + " Werkzeug 2.1. 'Request' now includes the functionality" + " directly.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) # type: ignore + + +class WWWAuthenticateMixin: + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'WWWAuthenticateMixin' is deprecated and will be removed" + " in Werkzeug 2.1. 'Response' now includes the" + " functionality directly.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) # type: ignore diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/base_request.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/base_request.py new file mode 100644 index 000000000..451989fd7 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/base_request.py @@ -0,0 +1,36 @@ +import typing as t +import warnings + +from .request import Request + + +class _FakeSubclassCheck(type): + def __subclasscheck__(cls, subclass: t.Type) -> bool: + warnings.warn( + "'BaseRequest' is deprecated and will be removed in" + " Werkzeug 2.1. Use 'issubclass(cls, Request)' instead.", + DeprecationWarning, + stacklevel=2, + ) + return issubclass(subclass, Request) + + def __instancecheck__(cls, instance: t.Any) -> bool: + warnings.warn( + "'BaseRequest' is deprecated and will be removed in" + " Werkzeug 2.1. Use 'isinstance(obj, Request)' instead.", + DeprecationWarning, + stacklevel=2, + ) + return isinstance(instance, Request) + + +class BaseRequest(Request, metaclass=_FakeSubclassCheck): + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'BaseRequest' is deprecated and will be removed in" + " Werkzeug 2.1. 'Request' now includes the functionality" + " directly.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/base_response.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/base_response.py new file mode 100644 index 000000000..3e0dc6766 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/base_response.py @@ -0,0 +1,36 @@ +import typing as t +import warnings + +from .response import Response + + +class _FakeSubclassCheck(type): + def __subclasscheck__(cls, subclass: t.Type) -> bool: + warnings.warn( + "'BaseResponse' is deprecated and will be removed in" + " Werkzeug 2.1. Use 'issubclass(cls, Response)' instead.", + DeprecationWarning, + stacklevel=2, + ) + return issubclass(subclass, Response) + + def __instancecheck__(cls, instance: t.Any) -> bool: + warnings.warn( + "'BaseResponse' is deprecated and will be removed in" + " Werkzeug 2.1. Use 'isinstance(obj, Response)' instead.", + DeprecationWarning, + stacklevel=2, + ) + return isinstance(instance, Response) + + +class BaseResponse(Response, metaclass=_FakeSubclassCheck): + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'BaseResponse' is deprecated and will be removed in" + " Werkzeug 2.1. 'Response' now includes the functionality" + " directly.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/common_descriptors.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/common_descriptors.py new file mode 100644 index 000000000..db87ea5fa --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/common_descriptors.py @@ -0,0 +1,26 @@ +import typing as t +import warnings + + +class CommonRequestDescriptorsMixin: + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'CommonRequestDescriptorsMixin' is deprecated and will be" + " removed in Werkzeug 2.1. 'Request' now includes the" + " functionality directly.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) # type: ignore + + +class CommonResponseDescriptorsMixin: + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'CommonResponseDescriptorsMixin' is deprecated and will be" + " removed in Werkzeug 2.1. 'Response' now includes the" + " functionality directly.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) # type: ignore diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/cors.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/cors.py new file mode 100644 index 000000000..89cf83ef8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/cors.py @@ -0,0 +1,26 @@ +import typing as t +import warnings + + +class CORSRequestMixin: + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'CORSRequestMixin' is deprecated and will be removed in" + " Werkzeug 2.1. 'Request' now includes the functionality" + " directly.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) # type: ignore + + +class CORSResponseMixin: + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'CORSResponseMixin' is deprecated and will be removed in" + " Werkzeug 2.1. 'Response' now includes the functionality" + " directly.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) # type: ignore diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/etag.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/etag.py new file mode 100644 index 000000000..2e9015a58 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/etag.py @@ -0,0 +1,26 @@ +import typing as t +import warnings + + +class ETagRequestMixin: + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'ETagRequestMixin' is deprecated and will be removed in" + " Werkzeug 2.1. 'Request' now includes the functionality" + " directly.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) # type: ignore + + +class ETagResponseMixin: + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'ETagResponseMixin' is deprecated and will be removed in" + " Werkzeug 2.1. 'Response' now includes the functionality" + " directly.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) # type: ignore diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/json.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/json.py new file mode 100644 index 000000000..ab6ed7ba9 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/json.py @@ -0,0 +1,13 @@ +import typing as t +import warnings + + +class JSONMixin: + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'JSONMixin' is deprecated and will be removed in Werkzeug" + " 2.1. 'Request' now includes the functionality directly.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) # type: ignore diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/request.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/request.py new file mode 100644 index 000000000..700cda04c --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/request.py @@ -0,0 +1,660 @@ +import functools +import json +import typing +import typing as t +import warnings +from io import BytesIO + +from .._internal import _wsgi_decoding_dance +from ..datastructures import CombinedMultiDict +from ..datastructures import EnvironHeaders +from ..datastructures import FileStorage +from ..datastructures import ImmutableMultiDict +from ..datastructures import iter_multi_items +from ..datastructures import MultiDict +from ..formparser import default_stream_factory +from ..formparser import FormDataParser +from ..sansio.request import Request as _SansIORequest +from ..utils import cached_property +from ..utils import environ_property +from ..wsgi import _get_server +from ..wsgi import get_input_stream +from werkzeug.exceptions import BadRequest + +if t.TYPE_CHECKING: + import typing_extensions as te + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + + +class Request(_SansIORequest): + """Represents an incoming WSGI HTTP request, with headers and body + taken from the WSGI environment. Has properties and methods for + using the functionality defined by various HTTP specs. The data in + requests object is read-only. + + Text data is assumed to use UTF-8 encoding, which should be true for + the vast majority of modern clients. Using an encoding set by the + client is unsafe in Python due to extra encodings it provides, such + as ``zip``. To change the assumed encoding, subclass and replace + :attr:`charset`. + + :param environ: The WSGI environ is generated by the WSGI server and + contains information about the server configuration and client + request. + :param populate_request: Add this request object to the WSGI environ + as ``environ['werkzeug.request']``. Can be useful when + debugging. + :param shallow: Makes reading from :attr:`stream` (and any method + that would read from it) raise a :exc:`RuntimeError`. Useful to + prevent consuming the form data in middleware, which would make + it unavailable to the final application. + + .. versionchanged:: 2.0 + Combine ``BaseRequest`` and mixins into a single ``Request`` + class. Using the old classes is deprecated and will be removed + in Werkzeug 2.1. + + .. versionchanged:: 0.5 + Read-only mode is enforced with immutable classes for all data. + """ + + #: the maximum content length. This is forwarded to the form data + #: parsing function (:func:`parse_form_data`). When set and the + #: :attr:`form` or :attr:`files` attribute is accessed and the + #: parsing fails because more than the specified value is transmitted + #: a :exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception is raised. + #: + #: Have a look at :doc:`/request_data` for more details. + #: + #: .. versionadded:: 0.5 + max_content_length: t.Optional[int] = None + + #: the maximum form field size. This is forwarded to the form data + #: parsing function (:func:`parse_form_data`). When set and the + #: :attr:`form` or :attr:`files` attribute is accessed and the + #: data in memory for post data is longer than the specified value a + #: :exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception is raised. + #: + #: Have a look at :doc:`/request_data` for more details. + #: + #: .. versionadded:: 0.5 + max_form_memory_size: t.Optional[int] = None + + #: The form data parser that shoud be used. Can be replaced to customize + #: the form date parsing. + form_data_parser_class: t.Type[FormDataParser] = FormDataParser + + #: Disable the :attr:`data` property to avoid reading from the input + #: stream. + #: + #: .. deprecated:: 2.0 + #: Will be removed in Werkzeug 2.1. Create the request with + #: ``shallow=True`` instead. + #: + #: .. versionadded:: 0.9 + disable_data_descriptor: t.Optional[bool] = None + + #: The WSGI environment containing HTTP headers and information from + #: the WSGI server. + environ: "WSGIEnvironment" + + #: Set when creating the request object. If ``True``, reading from + #: the request body will cause a ``RuntimeException``. Useful to + #: prevent modifying the stream from middleware. + shallow: bool + + def __init__( + self, + environ: "WSGIEnvironment", + populate_request: bool = True, + shallow: bool = False, + ) -> None: + super().__init__( + method=environ.get("REQUEST_METHOD", "GET"), + scheme=environ.get("wsgi.url_scheme", "http"), + server=_get_server(environ), + root_path=_wsgi_decoding_dance( + environ.get("SCRIPT_NAME") or "", self.charset, self.encoding_errors + ), + path=_wsgi_decoding_dance( + environ.get("PATH_INFO") or "", self.charset, self.encoding_errors + ), + query_string=environ.get("QUERY_STRING", "").encode("latin1"), + headers=EnvironHeaders(environ), + remote_addr=environ.get("REMOTE_ADDR"), + ) + self.environ = environ + + if self.disable_data_descriptor is not None: + warnings.warn( + "'disable_data_descriptor' is deprecated and will be" + " removed in Werkzeug 2.1. Create the request with" + " 'shallow=True' instead.", + DeprecationWarning, + stacklevel=2, + ) + shallow = shallow or self.disable_data_descriptor + + self.shallow = shallow + + if populate_request and not shallow: + self.environ["werkzeug.request"] = self + + @classmethod + def from_values(cls, *args: t.Any, **kwargs: t.Any) -> "Request": + """Create a new request object based on the values provided. If + environ is given missing values are filled from there. This method is + useful for small scripts when you need to simulate a request from an URL. + Do not use this method for unittesting, there is a full featured client + object (:class:`Client`) that allows to create multipart requests, + support for cookies etc. + + This accepts the same options as the + :class:`~werkzeug.test.EnvironBuilder`. + + .. versionchanged:: 0.5 + This method now accepts the same arguments as + :class:`~werkzeug.test.EnvironBuilder`. Because of this the + `environ` parameter is now called `environ_overrides`. + + :return: request object + """ + from ..test import EnvironBuilder + + charset = kwargs.pop("charset", cls.charset) + kwargs["charset"] = charset + builder = EnvironBuilder(*args, **kwargs) + try: + return builder.get_request(cls) + finally: + builder.close() + + @classmethod + def application( + cls, f: t.Callable[["Request"], "WSGIApplication"] + ) -> "WSGIApplication": + """Decorate a function as responder that accepts the request as + the last argument. This works like the :func:`responder` + decorator but the function is passed the request object as the + last argument and the request object will be closed + automatically:: + + @Request.application + def my_wsgi_app(request): + return Response('Hello World!') + + As of Werkzeug 0.14 HTTP exceptions are automatically caught and + converted to responses instead of failing. + + :param f: the WSGI callable to decorate + :return: a new WSGI callable + """ + #: return a callable that wraps the -2nd argument with the request + #: and calls the function with all the arguments up to that one and + #: the request. The return value is then called with the latest + #: two arguments. This makes it possible to use this decorator for + #: both standalone WSGI functions as well as bound methods and + #: partially applied functions. + from ..exceptions import HTTPException + + @functools.wraps(f) + def application(*args): # type: ignore + request = cls(args[-2]) + with request: + try: + resp = f(*args[:-2] + (request,)) + except HTTPException as e: + resp = e.get_response(args[-2]) + return resp(*args[-2:]) + + return t.cast("WSGIApplication", application) + + def _get_file_stream( + self, + total_content_length: t.Optional[int], + content_type: t.Optional[str], + filename: t.Optional[str] = None, + content_length: t.Optional[int] = None, + ) -> t.IO[bytes]: + """Called to get a stream for the file upload. + + This must provide a file-like class with `read()`, `readline()` + and `seek()` methods that is both writeable and readable. + + The default implementation returns a temporary file if the total + content length is higher than 500KB. Because many browsers do not + provide a content length for the files only the total content + length matters. + + :param total_content_length: the total content length of all the + data in the request combined. This value + is guaranteed to be there. + :param content_type: the mimetype of the uploaded file. + :param filename: the filename of the uploaded file. May be `None`. + :param content_length: the length of this file. This value is usually + not provided because webbrowsers do not provide + this value. + """ + return default_stream_factory( + total_content_length=total_content_length, + filename=filename, + content_type=content_type, + content_length=content_length, + ) + + @property + def want_form_data_parsed(self) -> bool: + """``True`` if the request method carries content. By default + this is true if a ``Content-Type`` is sent. + + .. versionadded:: 0.8 + """ + return bool(self.environ.get("CONTENT_TYPE")) + + def make_form_data_parser(self) -> FormDataParser: + """Creates the form data parser. Instantiates the + :attr:`form_data_parser_class` with some parameters. + + .. versionadded:: 0.8 + """ + return self.form_data_parser_class( + self._get_file_stream, + self.charset, + self.encoding_errors, + self.max_form_memory_size, + self.max_content_length, + self.parameter_storage_class, + ) + + def _load_form_data(self) -> None: + """Method used internally to retrieve submitted data. After calling + this sets `form` and `files` on the request object to multi dicts + filled with the incoming form data. As a matter of fact the input + stream will be empty afterwards. You can also call this method to + force the parsing of the form data. + + .. versionadded:: 0.8 + """ + # abort early if we have already consumed the stream + if "form" in self.__dict__: + return + + if self.want_form_data_parsed: + parser = self.make_form_data_parser() + data = parser.parse( + self._get_stream_for_parsing(), + self.mimetype, + self.content_length, + self.mimetype_params, + ) + else: + data = ( + self.stream, + self.parameter_storage_class(), + self.parameter_storage_class(), + ) + + # inject the values into the instance dict so that we bypass + # our cached_property non-data descriptor. + d = self.__dict__ + d["stream"], d["form"], d["files"] = data + + def _get_stream_for_parsing(self) -> t.IO[bytes]: + """This is the same as accessing :attr:`stream` with the difference + that if it finds cached data from calling :meth:`get_data` first it + will create a new stream out of the cached data. + + .. versionadded:: 0.9.3 + """ + cached_data = getattr(self, "_cached_data", None) + if cached_data is not None: + return BytesIO(cached_data) + return self.stream + + def close(self) -> None: + """Closes associated resources of this request object. This + closes all file handles explicitly. You can also use the request + object in a with statement which will automatically close it. + + .. versionadded:: 0.9 + """ + files = self.__dict__.get("files") + for _key, value in iter_multi_items(files or ()): + value.close() + + def __enter__(self) -> "Request": + return self + + def __exit__(self, exc_type, exc_value, tb) -> None: # type: ignore + self.close() + + @cached_property + def stream(self) -> t.IO[bytes]: + """ + If the incoming form data was not encoded with a known mimetype + the data is stored unmodified in this stream for consumption. Most + of the time it is a better idea to use :attr:`data` which will give + you that data as a string. The stream only returns the data once. + + Unlike :attr:`input_stream` this stream is properly guarded that you + can't accidentally read past the length of the input. Werkzeug will + internally always refer to this stream to read data which makes it + possible to wrap this object with a stream that does filtering. + + .. versionchanged:: 0.9 + This stream is now always available but might be consumed by the + form parser later on. Previously the stream was only set if no + parsing happened. + """ + if self.shallow: + raise RuntimeError( + "This request was created with 'shallow=True', reading" + " from the input stream is disabled." + ) + + return get_input_stream(self.environ) + + input_stream = environ_property[t.IO[bytes]]( + "wsgi.input", + doc="""The WSGI input stream. + + In general it's a bad idea to use this one because you can + easily read past the boundary. Use the :attr:`stream` + instead.""", + ) + + @cached_property + def data(self) -> bytes: + """ + Contains the incoming request data as string in case it came with + a mimetype Werkzeug does not handle. + """ + return self.get_data(parse_form_data=True) + + @typing.overload + def get_data( # type: ignore + self, + cache: bool = True, + as_text: "te.Literal[False]" = False, + parse_form_data: bool = False, + ) -> bytes: + ... + + @typing.overload + def get_data( + self, + cache: bool = True, + as_text: "te.Literal[True]" = ..., + parse_form_data: bool = False, + ) -> str: + ... + + def get_data( + self, cache: bool = True, as_text: bool = False, parse_form_data: bool = False + ) -> t.Union[bytes, str]: + """This reads the buffered incoming data from the client into one + bytes object. By default this is cached but that behavior can be + changed by setting `cache` to `False`. + + Usually it's a bad idea to call this method without checking the + content length first as a client could send dozens of megabytes or more + to cause memory problems on the server. + + Note that if the form data was already parsed this method will not + return anything as form data parsing does not cache the data like + this method does. To implicitly invoke form data parsing function + set `parse_form_data` to `True`. When this is done the return value + of this method will be an empty string if the form parser handles + the data. This generally is not necessary as if the whole data is + cached (which is the default) the form parser will used the cached + data to parse the form data. Please be generally aware of checking + the content length first in any case before calling this method + to avoid exhausting server memory. + + If `as_text` is set to `True` the return value will be a decoded + string. + + .. versionadded:: 0.9 + """ + rv = getattr(self, "_cached_data", None) + if rv is None: + if parse_form_data: + self._load_form_data() + rv = self.stream.read() + if cache: + self._cached_data = rv + if as_text: + rv = rv.decode(self.charset, self.encoding_errors) + return rv # type: ignore + + @cached_property + def form(self) -> "ImmutableMultiDict[str, str]": + """The form parameters. By default an + :class:`~werkzeug.datastructures.ImmutableMultiDict` + is returned from this function. This can be changed by setting + :attr:`parameter_storage_class` to a different type. This might + be necessary if the order of the form data is important. + + Please keep in mind that file uploads will not end up here, but instead + in the :attr:`files` attribute. + + .. versionchanged:: 0.9 + + Previous to Werkzeug 0.9 this would only contain form data for POST + and PUT requests. + """ + self._load_form_data() + return self.form + + @cached_property + def values(self) -> "CombinedMultiDict[str, str]": + """A :class:`werkzeug.datastructures.CombinedMultiDict` that + combines :attr:`args` and :attr:`form`. + + For GET requests, only ``args`` are present, not ``form``. + + .. versionchanged:: 2.0 + For GET requests, only ``args`` are present, not ``form``. + """ + sources = [self.args] + + if self.method != "GET": + # GET requests can have a body, and some caching proxies + # might not treat that differently than a normal GET + # request, allowing form data to "invisibly" affect the + # cache without indication in the query string / URL. + sources.append(self.form) + + args = [] + + for d in sources: + if not isinstance(d, MultiDict): + d = MultiDict(d) + + args.append(d) + + return CombinedMultiDict(args) + + @cached_property + def files(self) -> "ImmutableMultiDict[str, FileStorage]": + """:class:`~werkzeug.datastructures.MultiDict` object containing + all uploaded files. Each key in :attr:`files` is the name from the + ````. Each value in :attr:`files` is a + Werkzeug :class:`~werkzeug.datastructures.FileStorage` object. + + It basically behaves like a standard file object you know from Python, + with the difference that it also has a + :meth:`~werkzeug.datastructures.FileStorage.save` function that can + store the file on the filesystem. + + Note that :attr:`files` will only contain data if the request method was + POST, PUT or PATCH and the ``

    `` that posted to the request had + ``enctype="multipart/form-data"``. It will be empty otherwise. + + See the :class:`~werkzeug.datastructures.MultiDict` / + :class:`~werkzeug.datastructures.FileStorage` documentation for + more details about the used data structure. + """ + self._load_form_data() + return self.files + + @property + def script_root(self) -> str: + """Alias for :attr:`self.root_path`. ``environ["SCRIPT_ROOT"]`` + without a trailing slash. + """ + return self.root_path + + @cached_property + def url_root(self) -> str: + """Alias for :attr:`root_url`. The URL with scheme, host, and + root path. For example, ``https://example.com/app/``. + """ + return self.root_url + + remote_user = environ_property[str]( + "REMOTE_USER", + doc="""If the server supports user authentication, and the + script is protected, this attribute contains the username the + user has authenticated as.""", + ) + is_multithread = environ_property[bool]( + "wsgi.multithread", + doc="""boolean that is `True` if the application is served by a + multithreaded WSGI server.""", + ) + is_multiprocess = environ_property[bool]( + "wsgi.multiprocess", + doc="""boolean that is `True` if the application is served by a + WSGI server that spawns multiple processes.""", + ) + is_run_once = environ_property[bool]( + "wsgi.run_once", + doc="""boolean that is `True` if the application will be + executed only once in a process lifetime. This is the case for + CGI for example, but it's not guaranteed that the execution only + happens one time.""", + ) + + # JSON + + #: A module or other object that has ``dumps`` and ``loads`` + #: functions that match the API of the built-in :mod:`json` module. + json_module = json + + @property + def json(self) -> t.Optional[t.Any]: + """The parsed JSON data if :attr:`mimetype` indicates JSON + (:mimetype:`application/json`, see :attr:`is_json`). + + Calls :meth:`get_json` with default arguments. + """ + return self.get_json() + + # Cached values for ``(silent=False, silent=True)``. Initialized + # with sentinel values. + _cached_json: t.Tuple[t.Any, t.Any] = (Ellipsis, Ellipsis) + + def get_json( + self, force: bool = False, silent: bool = False, cache: bool = True + ) -> t.Optional[t.Any]: + """Parse :attr:`data` as JSON. + + If the mimetype does not indicate JSON + (:mimetype:`application/json`, see :attr:`is_json`), this + returns ``None``. + + If parsing fails, :meth:`on_json_loading_failed` is called and + its return value is used as the return value. + + :param force: Ignore the mimetype and always try to parse JSON. + :param silent: Silence parsing errors and return ``None`` + instead. + :param cache: Store the parsed JSON to return for subsequent + calls. + """ + if cache and self._cached_json[silent] is not Ellipsis: + return self._cached_json[silent] + + if not (force or self.is_json): + return None + + data = self.get_data(cache=cache) + + try: + rv = self.json_module.loads(data) + except ValueError as e: + if silent: + rv = None + + if cache: + normal_rv, _ = self._cached_json + self._cached_json = (normal_rv, rv) + else: + rv = self.on_json_loading_failed(e) + + if cache: + _, silent_rv = self._cached_json + self._cached_json = (rv, silent_rv) + else: + if cache: + self._cached_json = (rv, rv) + + return rv + + def on_json_loading_failed(self, e: ValueError) -> t.Any: + """Called if :meth:`get_json` parsing fails and isn't silenced. + If this method returns a value, it is used as the return value + for :meth:`get_json`. The default implementation raises + :exc:`~werkzeug.exceptions.BadRequest`. + """ + raise BadRequest(f"Failed to decode JSON object: {e}") + + +class StreamOnlyMixin: + """Mixin to create a ``Request`` that disables the ``data``, + ``form``, and ``files`` properties. Only ``stream`` is available. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Create the request with + ``shallow=True`` instead. + + .. versionadded:: 0.9 + """ + + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'StreamOnlyMixin' is deprecated and will be removed in" + " Werkzeug 2.1. Create the request with 'shallow=True'" + " instead.", + DeprecationWarning, + stacklevel=2, + ) + kwargs["shallow"] = True + super().__init__(*args, **kwargs) # type: ignore + + +class PlainRequest(StreamOnlyMixin, Request): + """A request object without ``data``, ``form``, and ``files``. + + .. deprecated:: 2.0 + Will be removed in Werkzeug 2.1. Create the request with + ``shallow=True`` instead. + + .. versionadded:: 0.9 + """ + + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'PlainRequest' is deprecated and will be removed in" + " Werkzeug 2.1. Create the request with 'shallow=True'" + " instead.", + DeprecationWarning, + stacklevel=2, + ) + + # Don't show the DeprecationWarning for StreamOnlyMixin. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + super().__init__(*args, **kwargs) diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/response.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/response.py new file mode 100644 index 000000000..d365c4e09 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/response.py @@ -0,0 +1,890 @@ +import json +import typing +import typing as t +import warnings +from http import HTTPStatus + +from .._internal import _to_bytes +from ..datastructures import Headers +from ..http import remove_entity_headers +from ..sansio.response import Response as _SansIOResponse +from ..urls import iri_to_uri +from ..urls import url_join +from ..utils import cached_property +from ..wsgi import ClosingIterator +from ..wsgi import get_current_url +from werkzeug._internal import _get_environ +from werkzeug.http import generate_etag +from werkzeug.http import http_date +from werkzeug.http import is_resource_modified +from werkzeug.http import parse_etags +from werkzeug.http import parse_range_header +from werkzeug.wsgi import _RangeWrapper + +if t.TYPE_CHECKING: + import typing_extensions as te + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + + +def _warn_if_string(iterable: t.Iterable) -> None: + """Helper for the response objects to check if the iterable returned + to the WSGI server is not a string. + """ + if isinstance(iterable, str): + warnings.warn( + "Response iterable was set to a string. This will appear to" + " work but means that the server will send the data to the" + " client one character at a time. This is almost never" + " intended behavior, use 'response.data' to assign strings" + " to the response object.", + stacklevel=2, + ) + + +def _iter_encoded( + iterable: t.Iterable[t.Union[str, bytes]], charset: str +) -> t.Iterator[bytes]: + for item in iterable: + if isinstance(item, str): + yield item.encode(charset) + else: + yield item + + +def _clean_accept_ranges(accept_ranges: t.Union[bool, str]) -> str: + if accept_ranges is True: + return "bytes" + elif accept_ranges is False: + return "none" + elif isinstance(accept_ranges, str): + return accept_ranges + raise ValueError("Invalid accept_ranges value") + + +class Response(_SansIOResponse): + """Represents an outgoing WSGI HTTP response with body, status, and + headers. Has properties and methods for using the functionality + defined by various HTTP specs. + + The response body is flexible to support different use cases. The + simple form is passing bytes, or a string which will be encoded as + UTF-8. Passing an iterable of bytes or strings makes this a + streaming response. A generator is particularly useful for building + a CSV file in memory or using SSE (Server Sent Events). A file-like + object is also iterable, although the + :func:`~werkzeug.utils.send_file` helper should be used in that + case. + + The response object is itself a WSGI application callable. When + called (:meth:`__call__`) with ``environ`` and ``start_response``, + it will pass its status and headers to ``start_response`` then + return its body as an iterable. + + .. code-block:: python + + from werkzeug.wrappers.response import Response + + def index(): + return Response("Hello, World!") + + def application(environ, start_response): + path = environ.get("PATH_INFO") or "/" + + if path == "/": + response = index() + else: + response = Response("Not Found", status=404) + + return response(environ, start_response) + + :param response: The data for the body of the response. A string or + bytes, or tuple or list of strings or bytes, for a fixed-length + response, or any other iterable of strings or bytes for a + streaming response. Defaults to an empty body. + :param status: The status code for the response. Either an int, in + which case the default status message is added, or a string in + the form ``{code} {message}``, like ``404 Not Found``. Defaults + to 200. + :param headers: A :class:`~werkzeug.datastructures.Headers` object, + or a list of ``(key, value)`` tuples that will be converted to a + ``Headers`` object. + :param mimetype: The mime type (content type without charset or + other parameters) of the response. If the value starts with + ``text/`` (or matches some other special cases), the charset + will be added to create the ``content_type``. + :param content_type: The full content type of the response. + Overrides building the value from ``mimetype``. + :param direct_passthrough: Pass the response body directly through + as the WSGI iterable. This can be used when the body is a binary + file or other iterator of bytes, to skip some unnecessary + checks. Use :func:`~werkzeug.utils.send_file` instead of setting + this manually. + + .. versionchanged:: 2.0 + Combine ``BaseResponse`` and mixins into a single ``Response`` + class. Using the old classes is deprecated and will be removed + in Werkzeug 2.1. + + .. versionchanged:: 0.5 + The ``direct_passthrough`` parameter was added. + """ + + #: if set to `False` accessing properties on the response object will + #: not try to consume the response iterator and convert it into a list. + #: + #: .. versionadded:: 0.6.2 + #: + #: That attribute was previously called `implicit_seqence_conversion`. + #: (Notice the typo). If you did use this feature, you have to adapt + #: your code to the name change. + implicit_sequence_conversion = True + + #: Should this response object correct the location header to be RFC + #: conformant? This is true by default. + #: + #: .. versionadded:: 0.8 + autocorrect_location_header = True + + #: Should this response object automatically set the content-length + #: header if possible? This is true by default. + #: + #: .. versionadded:: 0.8 + automatically_set_content_length = True + + #: The response body to send as the WSGI iterable. A list of strings + #: or bytes represents a fixed-length response, any other iterable + #: is a streaming response. Strings are encoded to bytes as UTF-8. + #: + #: Do not set to a plain string or bytes, that will cause sending + #: the response to be very inefficient as it will iterate one byte + #: at a time. + response: t.Union[t.Iterable[str], t.Iterable[bytes]] + + def __init__( + self, + response: t.Optional[ + t.Union[t.Iterable[bytes], bytes, t.Iterable[str], str] + ] = None, + status: t.Optional[t.Union[int, str, HTTPStatus]] = None, + headers: t.Optional[ + t.Union[ + t.Mapping[str, t.Union[str, int, t.Iterable[t.Union[str, int]]]], + t.Iterable[t.Tuple[str, t.Union[str, int]]], + ] + ] = None, + mimetype: t.Optional[str] = None, + content_type: t.Optional[str] = None, + direct_passthrough: bool = False, + ) -> None: + super().__init__( + status=status, + headers=headers, + mimetype=mimetype, + content_type=content_type, + ) + + #: Pass the response body directly through as the WSGI iterable. + #: This can be used when the body is a binary file or other + #: iterator of bytes, to skip some unnecessary checks. Use + #: :func:`~werkzeug.utils.send_file` instead of setting this + #: manually. + self.direct_passthrough = direct_passthrough + self._on_close: t.List[t.Callable[[], t.Any]] = [] + + # we set the response after the headers so that if a class changes + # the charset attribute, the data is set in the correct charset. + if response is None: + self.response = [] + elif isinstance(response, (str, bytes, bytearray)): + self.set_data(response) + else: + self.response = response + + def call_on_close(self, func: t.Callable[[], t.Any]) -> t.Callable[[], t.Any]: + """Adds a function to the internal list of functions that should + be called as part of closing down the response. Since 0.7 this + function also returns the function that was passed so that this + can be used as a decorator. + + .. versionadded:: 0.6 + """ + self._on_close.append(func) + return func + + def __repr__(self) -> str: + if self.is_sequence: + body_info = f"{sum(map(len, self.iter_encoded()))} bytes" + else: + body_info = "streamed" if self.is_streamed else "likely-streamed" + return f"<{type(self).__name__} {body_info} [{self.status}]>" + + @classmethod + def force_type( + cls, response: "Response", environ: t.Optional["WSGIEnvironment"] = None + ) -> "Response": + """Enforce that the WSGI response is a response object of the current + type. Werkzeug will use the :class:`Response` internally in many + situations like the exceptions. If you call :meth:`get_response` on an + exception you will get back a regular :class:`Response` object, even + if you are using a custom subclass. + + This method can enforce a given response type, and it will also + convert arbitrary WSGI callables into response objects if an environ + is provided:: + + # convert a Werkzeug response object into an instance of the + # MyResponseClass subclass. + response = MyResponseClass.force_type(response) + + # convert any WSGI application into a response object + response = MyResponseClass.force_type(response, environ) + + This is especially useful if you want to post-process responses in + the main dispatcher and use functionality provided by your subclass. + + Keep in mind that this will modify response objects in place if + possible! + + :param response: a response object or wsgi application. + :param environ: a WSGI environment object. + :return: a response object. + """ + if not isinstance(response, Response): + if environ is None: + raise TypeError( + "cannot convert WSGI application into response" + " objects without an environ" + ) + + from ..test import run_wsgi_app + + response = Response(*run_wsgi_app(response, environ)) + + response.__class__ = cls + return response + + @classmethod + def from_app( + cls, app: "WSGIApplication", environ: "WSGIEnvironment", buffered: bool = False + ) -> "Response": + """Create a new response object from an application output. This + works best if you pass it an application that returns a generator all + the time. Sometimes applications may use the `write()` callable + returned by the `start_response` function. This tries to resolve such + edge cases automatically. But if you don't get the expected output + you should set `buffered` to `True` which enforces buffering. + + :param app: the WSGI application to execute. + :param environ: the WSGI environment to execute against. + :param buffered: set to `True` to enforce buffering. + :return: a response object. + """ + from ..test import run_wsgi_app + + return cls(*run_wsgi_app(app, environ, buffered)) + + @typing.overload + def get_data(self, as_text: "te.Literal[False]" = False) -> bytes: + ... + + @typing.overload + def get_data(self, as_text: "te.Literal[True]") -> str: + ... + + def get_data(self, as_text: bool = False) -> t.Union[bytes, str]: + """The string representation of the response body. Whenever you call + this property the response iterable is encoded and flattened. This + can lead to unwanted behavior if you stream big data. + + This behavior can be disabled by setting + :attr:`implicit_sequence_conversion` to `False`. + + If `as_text` is set to `True` the return value will be a decoded + string. + + .. versionadded:: 0.9 + """ + self._ensure_sequence() + rv = b"".join(self.iter_encoded()) + + if as_text: + return rv.decode(self.charset) + + return rv + + def set_data(self, value: t.Union[bytes, str]) -> None: + """Sets a new string as response. The value must be a string or + bytes. If a string is set it's encoded to the charset of the + response (utf-8 by default). + + .. versionadded:: 0.9 + """ + # if a string is set, it's encoded directly so that we + # can set the content length + if isinstance(value, str): + value = value.encode(self.charset) + else: + value = bytes(value) + self.response = [value] + if self.automatically_set_content_length: + self.headers["Content-Length"] = str(len(value)) + + data = property( + get_data, + set_data, + doc="A descriptor that calls :meth:`get_data` and :meth:`set_data`.", + ) + + def calculate_content_length(self) -> t.Optional[int]: + """Returns the content length if available or `None` otherwise.""" + try: + self._ensure_sequence() + except RuntimeError: + return None + return sum(len(x) for x in self.iter_encoded()) + + def _ensure_sequence(self, mutable: bool = False) -> None: + """This method can be called by methods that need a sequence. If + `mutable` is true, it will also ensure that the response sequence + is a standard Python list. + + .. versionadded:: 0.6 + """ + if self.is_sequence: + # if we need a mutable object, we ensure it's a list. + if mutable and not isinstance(self.response, list): + self.response = list(self.response) # type: ignore + return + if self.direct_passthrough: + raise RuntimeError( + "Attempted implicit sequence conversion but the" + " response object is in direct passthrough mode." + ) + if not self.implicit_sequence_conversion: + raise RuntimeError( + "The response object required the iterable to be a" + " sequence, but the implicit conversion was disabled." + " Call make_sequence() yourself." + ) + self.make_sequence() + + def make_sequence(self) -> None: + """Converts the response iterator in a list. By default this happens + automatically if required. If `implicit_sequence_conversion` is + disabled, this method is not automatically called and some properties + might raise exceptions. This also encodes all the items. + + .. versionadded:: 0.6 + """ + if not self.is_sequence: + # if we consume an iterable we have to ensure that the close + # method of the iterable is called if available when we tear + # down the response + close = getattr(self.response, "close", None) + self.response = list(self.iter_encoded()) + if close is not None: + self.call_on_close(close) + + def iter_encoded(self) -> t.Iterator[bytes]: + """Iter the response encoded with the encoding of the response. + If the response object is invoked as WSGI application the return + value of this method is used as application iterator unless + :attr:`direct_passthrough` was activated. + """ + if __debug__: + _warn_if_string(self.response) + # Encode in a separate function so that self.response is fetched + # early. This allows us to wrap the response with the return + # value from get_app_iter or iter_encoded. + return _iter_encoded(self.response, self.charset) + + @property + def is_streamed(self) -> bool: + """If the response is streamed (the response is not an iterable with + a length information) this property is `True`. In this case streamed + means that there is no information about the number of iterations. + This is usually `True` if a generator is passed to the response object. + + This is useful for checking before applying some sort of post + filtering that should not take place for streamed responses. + """ + try: + len(self.response) # type: ignore + except (TypeError, AttributeError): + return True + return False + + @property + def is_sequence(self) -> bool: + """If the iterator is buffered, this property will be `True`. A + response object will consider an iterator to be buffered if the + response attribute is a list or tuple. + + .. versionadded:: 0.6 + """ + return isinstance(self.response, (tuple, list)) + + def close(self) -> None: + """Close the wrapped response if possible. You can also use the object + in a with statement which will automatically close it. + + .. versionadded:: 0.9 + Can now be used in a with statement. + """ + if hasattr(self.response, "close"): + self.response.close() # type: ignore + for func in self._on_close: + func() + + def __enter__(self) -> "Response": + return self + + def __exit__(self, exc_type, exc_value, tb): # type: ignore + self.close() + + def freeze(self, no_etag: None = None) -> None: + """Make the response object ready to be pickled. Does the + following: + + * Buffer the response into a list, ignoring + :attr:`implicity_sequence_conversion` and + :attr:`direct_passthrough`. + * Set the ``Content-Length`` header. + * Generate an ``ETag`` header if one is not already set. + + .. versionchanged:: 2.0 + An ``ETag`` header is added, the ``no_etag`` parameter is + deprecated and will be removed in Werkzeug 2.1. + + .. versionchanged:: 0.6 + The ``Content-Length`` header is set. + """ + # Always freeze the encoded response body, ignore + # implicit_sequence_conversion and direct_passthrough. + self.response = list(self.iter_encoded()) + self.headers["Content-Length"] = str(sum(map(len, self.response))) + + if no_etag is not None: + warnings.warn( + "The 'no_etag' parameter is deprecated and will be" + " removed in Werkzeug 2.1.", + DeprecationWarning, + stacklevel=2, + ) + + self.add_etag() + + def get_wsgi_headers(self, environ: "WSGIEnvironment") -> Headers: + """This is automatically called right before the response is started + and returns headers modified for the given environment. It returns a + copy of the headers from the response with some modifications applied + if necessary. + + For example the location header (if present) is joined with the root + URL of the environment. Also the content length is automatically set + to zero here for certain status codes. + + .. versionchanged:: 0.6 + Previously that function was called `fix_headers` and modified + the response object in place. Also since 0.6, IRIs in location + and content-location headers are handled properly. + + Also starting with 0.6, Werkzeug will attempt to set the content + length if it is able to figure it out on its own. This is the + case if all the strings in the response iterable are already + encoded and the iterable is buffered. + + :param environ: the WSGI environment of the request. + :return: returns a new :class:`~werkzeug.datastructures.Headers` + object. + """ + headers = Headers(self.headers) + location: t.Optional[str] = None + content_location: t.Optional[str] = None + content_length: t.Optional[t.Union[str, int]] = None + status = self.status_code + + # iterate over the headers to find all values in one go. Because + # get_wsgi_headers is used each response that gives us a tiny + # speedup. + for key, value in headers: + ikey = key.lower() + if ikey == "location": + location = value + elif ikey == "content-location": + content_location = value + elif ikey == "content-length": + content_length = value + + # make sure the location header is an absolute URL + if location is not None: + old_location = location + if isinstance(location, str): + # Safe conversion is necessary here as we might redirect + # to a broken URI scheme (for instance itms-services). + location = iri_to_uri(location, safe_conversion=True) + + if self.autocorrect_location_header: + current_url = get_current_url(environ, strip_querystring=True) + if isinstance(current_url, str): + current_url = iri_to_uri(current_url) + location = url_join(current_url, location) + if location != old_location: + headers["Location"] = location + + # make sure the content location is a URL + if content_location is not None and isinstance(content_location, str): + headers["Content-Location"] = iri_to_uri(content_location) + + if 100 <= status < 200 or status == 204: + # Per section 3.3.2 of RFC 7230, "a server MUST NOT send a + # Content-Length header field in any response with a status + # code of 1xx (Informational) or 204 (No Content)." + headers.remove("Content-Length") + elif status == 304: + remove_entity_headers(headers) + + # if we can determine the content length automatically, we + # should try to do that. But only if this does not involve + # flattening the iterator or encoding of strings in the + # response. We however should not do that if we have a 304 + # response. + if ( + self.automatically_set_content_length + and self.is_sequence + and content_length is None + and status not in (204, 304) + and not (100 <= status < 200) + ): + try: + content_length = sum(len(_to_bytes(x, "ascii")) for x in self.response) + except UnicodeError: + # Something other than bytes, can't safely figure out + # the length of the response. + pass + else: + headers["Content-Length"] = str(content_length) + + return headers + + def get_app_iter(self, environ: "WSGIEnvironment") -> t.Iterable[bytes]: + """Returns the application iterator for the given environ. Depending + on the request method and the current status code the return value + might be an empty response rather than the one from the response. + + If the request method is `HEAD` or the status code is in a range + where the HTTP specification requires an empty response, an empty + iterable is returned. + + .. versionadded:: 0.6 + + :param environ: the WSGI environment of the request. + :return: a response iterable. + """ + status = self.status_code + if ( + environ["REQUEST_METHOD"] == "HEAD" + or 100 <= status < 200 + or status in (204, 304) + ): + iterable: t.Iterable[bytes] = () + elif self.direct_passthrough: + if __debug__: + _warn_if_string(self.response) + return self.response # type: ignore + else: + iterable = self.iter_encoded() + return ClosingIterator(iterable, self.close) + + def get_wsgi_response( + self, environ: "WSGIEnvironment" + ) -> t.Tuple[t.Iterable[bytes], str, t.List[t.Tuple[str, str]]]: + """Returns the final WSGI response as tuple. The first item in + the tuple is the application iterator, the second the status and + the third the list of headers. The response returned is created + specially for the given environment. For example if the request + method in the WSGI environment is ``'HEAD'`` the response will + be empty and only the headers and status code will be present. + + .. versionadded:: 0.6 + + :param environ: the WSGI environment of the request. + :return: an ``(app_iter, status, headers)`` tuple. + """ + headers = self.get_wsgi_headers(environ) + app_iter = self.get_app_iter(environ) + return app_iter, self.status, headers.to_wsgi_list() + + def __call__( + self, environ: "WSGIEnvironment", start_response: "StartResponse" + ) -> t.Iterable[bytes]: + """Process this response as WSGI application. + + :param environ: the WSGI environment. + :param start_response: the response callable provided by the WSGI + server. + :return: an application iterator + """ + app_iter, status, headers = self.get_wsgi_response(environ) + start_response(status, headers) + return app_iter + + # JSON + + #: A module or other object that has ``dumps`` and ``loads`` + #: functions that match the API of the built-in :mod:`json` module. + json_module = json + + @property + def json(self) -> t.Optional[t.Any]: + """The parsed JSON data if :attr:`mimetype` indicates JSON + (:mimetype:`application/json`, see :attr:`is_json`). + + Calls :meth:`get_json` with default arguments. + """ + return self.get_json() + + def get_json(self, force: bool = False, silent: bool = False) -> t.Optional[t.Any]: + """Parse :attr:`data` as JSON. Useful during testing. + + If the mimetype does not indicate JSON + (:mimetype:`application/json`, see :attr:`is_json`), this + returns ``None``. + + Unlike :meth:`Request.get_json`, the result is not cached. + + :param force: Ignore the mimetype and always try to parse JSON. + :param silent: Silence parsing errors and return ``None`` + instead. + """ + if not (force or self.is_json): + return None + + data = self.get_data() + + try: + return self.json_module.loads(data) + except ValueError: + if not silent: + raise + + return None + + # Stream + + @cached_property + def stream(self) -> "ResponseStream": + """The response iterable as write-only stream.""" + return ResponseStream(self) + + def _wrap_range_response(self, start: int, length: int) -> None: + """Wrap existing Response in case of Range Request context.""" + if self.status_code == 206: + self.response = _RangeWrapper(self.response, start, length) # type: ignore + + def _is_range_request_processable(self, environ: "WSGIEnvironment") -> bool: + """Return ``True`` if `Range` header is present and if underlying + resource is considered unchanged when compared with `If-Range` header. + """ + return ( + "HTTP_IF_RANGE" not in environ + or not is_resource_modified( + environ, + self.headers.get("etag"), + None, + self.headers.get("last-modified"), + ignore_if_range=False, + ) + ) and "HTTP_RANGE" in environ + + def _process_range_request( + self, + environ: "WSGIEnvironment", + complete_length: t.Optional[int] = None, + accept_ranges: t.Optional[t.Union[bool, str]] = None, + ) -> bool: + """Handle Range Request related headers (RFC7233). If `Accept-Ranges` + header is valid, and Range Request is processable, we set the headers + as described by the RFC, and wrap the underlying response in a + RangeWrapper. + + Returns ``True`` if Range Request can be fulfilled, ``False`` otherwise. + + :raises: :class:`~werkzeug.exceptions.RequestedRangeNotSatisfiable` + if `Range` header could not be parsed or satisfied. + + .. versionchanged:: 2.0 + Returns ``False`` if the length is 0. + """ + from ..exceptions import RequestedRangeNotSatisfiable + + if ( + accept_ranges is None + or complete_length is None + or complete_length == 0 + or not self._is_range_request_processable(environ) + ): + return False + + parsed_range = parse_range_header(environ.get("HTTP_RANGE")) + + if parsed_range is None: + raise RequestedRangeNotSatisfiable(complete_length) + + range_tuple = parsed_range.range_for_length(complete_length) + content_range_header = parsed_range.to_content_range_header(complete_length) + + if range_tuple is None or content_range_header is None: + raise RequestedRangeNotSatisfiable(complete_length) + + content_length = range_tuple[1] - range_tuple[0] + self.headers["Content-Length"] = content_length + self.headers["Accept-Ranges"] = accept_ranges + self.content_range = content_range_header # type: ignore + self.status_code = 206 + self._wrap_range_response(range_tuple[0], content_length) + return True + + def make_conditional( + self, + request_or_environ: "WSGIEnvironment", + accept_ranges: t.Union[bool, str] = False, + complete_length: t.Optional[int] = None, + ) -> "Response": + """Make the response conditional to the request. This method works + best if an etag was defined for the response already. The `add_etag` + method can be used to do that. If called without etag just the date + header is set. + + This does nothing if the request method in the request or environ is + anything but GET or HEAD. + + For optimal performance when handling range requests, it's recommended + that your response data object implements `seekable`, `seek` and `tell` + methods as described by :py:class:`io.IOBase`. Objects returned by + :meth:`~werkzeug.wsgi.wrap_file` automatically implement those methods. + + It does not remove the body of the response because that's something + the :meth:`__call__` function does for us automatically. + + Returns self so that you can do ``return resp.make_conditional(req)`` + but modifies the object in-place. + + :param request_or_environ: a request object or WSGI environment to be + used to make the response conditional + against. + :param accept_ranges: This parameter dictates the value of + `Accept-Ranges` header. If ``False`` (default), + the header is not set. If ``True``, it will be set + to ``"bytes"``. If ``None``, it will be set to + ``"none"``. If it's a string, it will use this + value. + :param complete_length: Will be used only in valid Range Requests. + It will set `Content-Range` complete length + value and compute `Content-Length` real value. + This parameter is mandatory for successful + Range Requests completion. + :raises: :class:`~werkzeug.exceptions.RequestedRangeNotSatisfiable` + if `Range` header could not be parsed or satisfied. + + .. versionchanged:: 2.0 + Range processing is skipped if length is 0 instead of + raising a 416 Range Not Satisfiable error. + """ + environ = _get_environ(request_or_environ) + if environ["REQUEST_METHOD"] in ("GET", "HEAD"): + # if the date is not in the headers, add it now. We however + # will not override an already existing header. Unfortunately + # this header will be overriden by many WSGI servers including + # wsgiref. + if "date" not in self.headers: + self.headers["Date"] = http_date() + accept_ranges = _clean_accept_ranges(accept_ranges) + is206 = self._process_range_request(environ, complete_length, accept_ranges) + if not is206 and not is_resource_modified( + environ, + self.headers.get("etag"), + None, + self.headers.get("last-modified"), + ): + if parse_etags(environ.get("HTTP_IF_MATCH")): + self.status_code = 412 + else: + self.status_code = 304 + if ( + self.automatically_set_content_length + and "content-length" not in self.headers + ): + length = self.calculate_content_length() + if length is not None: + self.headers["Content-Length"] = length + return self + + def add_etag(self, overwrite: bool = False, weak: bool = False) -> None: + """Add an etag for the current response if there is none yet. + + .. versionchanged:: 2.0 + SHA-1 is used to generate the value. MD5 may not be + available in some environments. + """ + if overwrite or "etag" not in self.headers: + self.set_etag(generate_etag(self.get_data()), weak) + + +class ResponseStream: + """A file descriptor like object used by the :class:`ResponseStreamMixin` to + represent the body of the stream. It directly pushes into the response + iterable of the response object. + """ + + mode = "wb+" + + def __init__(self, response: Response): + self.response = response + self.closed = False + + def write(self, value: bytes) -> int: + if self.closed: + raise ValueError("I/O operation on closed file") + self.response._ensure_sequence(mutable=True) + self.response.response.append(value) # type: ignore + self.response.headers.pop("Content-Length", None) + return len(value) + + def writelines(self, seq: t.Iterable[bytes]) -> None: + for item in seq: + self.write(item) + + def close(self) -> None: + self.closed = True + + def flush(self) -> None: + if self.closed: + raise ValueError("I/O operation on closed file") + + def isatty(self) -> bool: + if self.closed: + raise ValueError("I/O operation on closed file") + return False + + def tell(self) -> int: + self.response._ensure_sequence() + return sum(map(len, self.response.response)) + + @property + def encoding(self) -> str: + return self.response.charset + + +class ResponseStreamMixin: + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'ResponseStreamMixin' is deprecated and will be removed in" + " Werkzeug 2.1. 'Response' now includes the functionality" + " directly.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) # type: ignore diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/user_agent.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/user_agent.py new file mode 100644 index 000000000..184ffd023 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wrappers/user_agent.py @@ -0,0 +1,14 @@ +import typing as t +import warnings + + +class UserAgentMixin: + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + warnings.warn( + "'UserAgentMixin' is deprecated and will be removed in" + " Werkzeug 2.1. 'Request' now includes the functionality" + " directly.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) # type: ignore diff --git a/flask-app/venv/lib/python3.8/site-packages/werkzeug/wsgi.py b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wsgi.py new file mode 100644 index 000000000..9cfa74de8 --- /dev/null +++ b/flask-app/venv/lib/python3.8/site-packages/werkzeug/wsgi.py @@ -0,0 +1,982 @@ +import io +import re +import typing as t +from functools import partial +from functools import update_wrapper +from itertools import chain + +from ._internal import _make_encode_wrapper +from ._internal import _to_bytes +from ._internal import _to_str +from .sansio import utils as _sansio_utils +from .sansio.utils import host_is_trusted # noqa: F401 # Imported as part of API +from .urls import _URLTuple +from .urls import uri_to_iri +from .urls import url_join +from .urls import url_parse +from .urls import url_quote + +if t.TYPE_CHECKING: + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + + +def responder(f: t.Callable[..., "WSGIApplication"]) -> "WSGIApplication": + """Marks a function as responder. Decorate a function with it and it + will automatically call the return value as WSGI application. + + Example:: + + @responder + def application(environ, start_response): + return Response('Hello World!') + """ + return update_wrapper(lambda *a: f(*a)(*a[-2:]), f) + + +def get_current_url( + environ: "WSGIEnvironment", + root_only: bool = False, + strip_querystring: bool = False, + host_only: bool = False, + trusted_hosts: t.Optional[t.Iterable[str]] = None, +) -> str: + """Recreate the URL for a request from the parts in a WSGI + environment. + + The URL is an IRI, not a URI, so it may contain Unicode characters. + Use :func:`~werkzeug.urls.iri_to_uri` to convert it to ASCII. + + :param environ: The WSGI environment to get the URL parts from. + :param root_only: Only build the root path, don't include the + remaining path or query string. + :param strip_querystring: Don't include the query string. + :param host_only: Only build the scheme and host. + :param trusted_hosts: A list of trusted host names to validate the + host against. + """ + parts = { + "scheme": environ["wsgi.url_scheme"], + "host": get_host(environ, trusted_hosts), + } + + if not host_only: + parts["root_path"] = environ.get("SCRIPT_NAME", "") + + if not root_only: + parts["path"] = environ.get("PATH_INFO", "") + + if not strip_querystring: + parts["query_string"] = environ.get("QUERY_STRING", "").encode("latin1") + + return _sansio_utils.get_current_url(**parts) + + +def _get_server( + environ: "WSGIEnvironment", +) -> t.Optional[t.Tuple[str, t.Optional[int]]]: + name = environ.get("SERVER_NAME") + + if name is None: + return None + + try: + port: t.Optional[int] = int(environ.get("SERVER_PORT", None)) + except (TypeError, ValueError): + # unix socket + port = None + + return name, port + + +def get_host( + environ: "WSGIEnvironment", trusted_hosts: t.Optional[t.Iterable[str]] = None +) -> str: + """Return the host for the given WSGI environment. + + The ``Host`` header is preferred, then ``SERVER_NAME`` if it's not + set. The returned host will only contain the port if it is different + than the standard port for the protocol. + + Optionally, verify that the host is trusted using + :func:`host_is_trusted` and raise a + :exc:`~werkzeug.exceptions.SecurityError` if it is not. + + :param environ: A WSGI environment dict. + :param trusted_hosts: A list of trusted host names. + + :return: Host, with port if necessary. + :raise ~werkzeug.exceptions.SecurityError: If the host is not + trusted. + """ + return _sansio_utils.get_host( + environ["wsgi.url_scheme"], + environ.get("HTTP_HOST"), + _get_server(environ), + trusted_hosts, + ) + + +def get_content_length(environ: "WSGIEnvironment") -> t.Optional[int]: + """Returns the content length from the WSGI environment as + integer. If it's not available or chunked transfer encoding is used, + ``None`` is returned. + + .. versionadded:: 0.9 + + :param environ: the WSGI environ to fetch the content length from. + """ + if environ.get("HTTP_TRANSFER_ENCODING", "") == "chunked": + return None + + content_length = environ.get("CONTENT_LENGTH") + if content_length is not None: + try: + return max(0, int(content_length)) + except (ValueError, TypeError): + pass + return None + + +def get_input_stream( + environ: "WSGIEnvironment", safe_fallback: bool = True +) -> t.IO[bytes]: + """Returns the input stream from the WSGI environment and wraps it + in the most sensible way possible. The stream returned is not the + raw WSGI stream in most cases but one that is safe to read from + without taking into account the content length. + + If content length is not set, the stream will be empty for safety reasons. + If the WSGI server supports chunked or infinite streams, it should set + the ``wsgi.input_terminated`` value in the WSGI environ to indicate that. + + .. versionadded:: 0.9 + + :param environ: the WSGI environ to fetch the stream from. + :param safe_fallback: use an empty stream as a safe fallback when the + content length is not set. Disabling this allows infinite streams, + which can be a denial-of-service risk. + """ + stream = t.cast(t.IO[bytes], environ["wsgi.input"]) + content_length = get_content_length(environ) + + # A wsgi extension that tells us if the input is terminated. In + # that case we return the stream unchanged as we know we can safely + # read it until the end. + if environ.get("wsgi.input_terminated"): + return stream + + # If the request doesn't specify a content length, returning the stream is + # potentially dangerous because it could be infinite, malicious or not. If + # safe_fallback is true, return an empty stream instead for safety. + if content_length is None: + return io.BytesIO() if safe_fallback else stream + + # Otherwise limit the stream to the content length + return t.cast(t.IO[bytes], LimitedStream(stream, content_length)) + + +def get_query_string(environ: "WSGIEnvironment") -> str: + """Returns the ``QUERY_STRING`` from the WSGI environment. This also + takes care of the WSGI decoding dance. The string returned will be + restricted to ASCII characters. + + :param environ: WSGI environment to get the query string from. + + .. versionadded:: 0.9 + """ + qs = environ.get("QUERY_STRING", "").encode("latin1") + # QUERY_STRING really should be ascii safe but some browsers + # will send us some unicode stuff (I am looking at you IE). + # In that case we want to urllib quote it badly. + return url_quote(qs, safe=":&%=+$!*'(),") + + +def get_path_info( + environ: "WSGIEnvironment", charset: str = "utf-8", errors: str = "replace" +) -> str: + """Return the ``PATH_INFO`` from the WSGI environment and decode it + unless ``charset`` is ``None``. + + :param environ: WSGI environment to get the path from. + :param charset: The charset for the path info, or ``None`` if no + decoding should be performed. + :param errors: The decoding error handling. + + .. versionadded:: 0.9 + """ + path = environ.get("PATH_INFO", "").encode("latin1") + return _to_str(path, charset, errors, allow_none_charset=True) # type: ignore + + +def get_script_name( + environ: "WSGIEnvironment", charset: str = "utf-8", errors: str = "replace" +) -> str: + """Return the ``SCRIPT_NAME`` from the WSGI environment and decode + it unless `charset` is set to ``None``. + + :param environ: WSGI environment to get the path from. + :param charset: The charset for the path, or ``None`` if no decoding + should be performed. + :param errors: The decoding error handling. + + .. versionadded:: 0.9 + """ + path = environ.get("SCRIPT_NAME", "").encode("latin1") + return _to_str(path, charset, errors, allow_none_charset=True) # type: ignore + + +def pop_path_info( + environ: "WSGIEnvironment", charset: str = "utf-8", errors: str = "replace" +) -> t.Optional[str]: + """Removes and returns the next segment of `PATH_INFO`, pushing it onto + `SCRIPT_NAME`. Returns `None` if there is nothing left on `PATH_INFO`. + + If the `charset` is set to `None` bytes are returned. + + If there are empty segments (``'/foo//bar``) these are ignored but + properly pushed to the `SCRIPT_NAME`: + + >>> env = {'SCRIPT_NAME': '/foo', 'PATH_INFO': '/a/b'} + >>> pop_path_info(env) + 'a' + >>> env['SCRIPT_NAME'] + '/foo/a' + >>> pop_path_info(env) + 'b' + >>> env['SCRIPT_NAME'] + '/foo/a/b' + + .. versionadded:: 0.5 + + .. versionchanged:: 0.9 + The path is now decoded and a charset and encoding + parameter can be provided. + + :param environ: the WSGI environment that is modified. + :param charset: The ``encoding`` parameter passed to + :func:`bytes.decode`. + :param errors: The ``errors`` paramater passed to + :func:`bytes.decode`. + """ + path = environ.get("PATH_INFO") + if not path: + return None + + script_name = environ.get("SCRIPT_NAME", "") + + # shift multiple leading slashes over + old_path = path + path = path.lstrip("/") + if path != old_path: + script_name += "/" * (len(old_path) - len(path)) + + if "/" not in path: + environ["PATH_INFO"] = "" + environ["SCRIPT_NAME"] = script_name + path + rv = path.encode("latin1") + else: + segment, path = path.split("/", 1) + environ["PATH_INFO"] = f"/{path}" + environ["SCRIPT_NAME"] = script_name + segment + rv = segment.encode("latin1") + + return _to_str(rv, charset, errors, allow_none_charset=True) # type: ignore + + +def peek_path_info( + environ: "WSGIEnvironment", charset: str = "utf-8", errors: str = "replace" +) -> t.Optional[str]: + """Returns the next segment on the `PATH_INFO` or `None` if there + is none. Works like :func:`pop_path_info` without modifying the + environment: + + >>> env = {'SCRIPT_NAME': '/foo', 'PATH_INFO': '/a/b'} + >>> peek_path_info(env) + 'a' + >>> peek_path_info(env) + 'a' + + If the `charset` is set to `None` bytes are returned. + + .. versionadded:: 0.5 + + .. versionchanged:: 0.9 + The path is now decoded and a charset and encoding + parameter can be provided. + + :param environ: the WSGI environment that is checked. + """ + segments = environ.get("PATH_INFO", "").lstrip("/").split("/", 1) + if segments: + return _to_str( # type: ignore + segments[0].encode("latin1"), charset, errors, allow_none_charset=True + ) + return None + + +def extract_path_info( + environ_or_baseurl: t.Union[str, "WSGIEnvironment"], + path_or_url: t.Union[str, _URLTuple], + charset: str = "utf-8", + errors: str = "werkzeug.url_quote", + collapse_http_schemes: bool = True, +) -> t.Optional[str]: + """Extracts the path info from the given URL (or WSGI environment) and + path. The path info returned is a string. The URLs might also be IRIs. + + If the path info could not be determined, `None` is returned. + + Some examples: + + >>> extract_path_info('http://example.com/app', '/app/hello') + '/hello' + >>> extract_path_info('http://example.com/app', + ... 'https://example.com/app/hello') + '/hello' + >>> extract_path_info('http://example.com/app', + ... 'https://example.com/app/hello', + ... collapse_http_schemes=False) is None + True + + Instead of providing a base URL you can also pass a WSGI environment. + + :param environ_or_baseurl: a WSGI environment dict, a base URL or + base IRI. This is the root of the + application. + :param path_or_url: an absolute path from the server root, a + relative path (in which case it's the path info) + or a full URL. + :param charset: the charset for byte data in URLs + :param errors: the error handling on decode + :param collapse_http_schemes: if set to `False` the algorithm does + not assume that http and https on the + same server point to the same + resource. + + .. versionchanged:: 0.15 + The ``errors`` parameter defaults to leaving invalid bytes + quoted instead of replacing them. + + .. versionadded:: 0.6 + """ + + def _normalize_netloc(scheme: str, netloc: str) -> str: + parts = netloc.split("@", 1)[-1].split(":", 1) + port: t.Optional[str] + + if len(parts) == 2: + netloc, port = parts + if (scheme == "http" and port == "80") or ( + scheme == "https" and port == "443" + ): + port = None + else: + netloc = parts[0] + port = None + + if port is not None: + netloc += f":{port}" + + return netloc + + # make sure whatever we are working on is a IRI and parse it + path = uri_to_iri(path_or_url, charset, errors) + if isinstance(environ_or_baseurl, dict): + environ_or_baseurl = get_current_url(environ_or_baseurl, root_only=True) + base_iri = uri_to_iri(environ_or_baseurl, charset, errors) + base_scheme, base_netloc, base_path = url_parse(base_iri)[:3] + cur_scheme, cur_netloc, cur_path = url_parse(url_join(base_iri, path))[:3] + + # normalize the network location + base_netloc = _normalize_netloc(base_scheme, base_netloc) + cur_netloc = _normalize_netloc(cur_scheme, cur_netloc) + + # is that IRI even on a known HTTP scheme? + if collapse_http_schemes: + for scheme in base_scheme, cur_scheme: + if scheme not in ("http", "https"): + return None + else: + if not (base_scheme in ("http", "https") and base_scheme == cur_scheme): + return None + + # are the netlocs compatible? + if base_netloc != cur_netloc: + return None + + # are we below the application path? + base_path = base_path.rstrip("/") + if not cur_path.startswith(base_path): + return None + + return f"/{cur_path[len(base_path) :].lstrip('/')}" + + +class ClosingIterator: + """The WSGI specification requires that all middlewares and gateways + respect the `close` callback of the iterable returned by the application. + Because it is useful to add another close action to a returned iterable + and adding a custom iterable is a boring task this class can be used for + that:: + + return ClosingIterator(app(environ, start_response), [cleanup_session, + cleanup_locals]) + + If there is just one close function it can be passed instead of the list. + + A closing iterator is not needed if the application uses response objects + and finishes the processing if the response is started:: + + try: + return response(environ, start_response) + finally: + cleanup_session() + cleanup_locals() + """ + + def __init__( + self, + iterable: t.Iterable[bytes], + callbacks: t.Optional[ + t.Union[t.Callable[[], None], t.Iterable[t.Callable[[], None]]] + ] = None, + ) -> None: + iterator = iter(iterable) + self._next = t.cast(t.Callable[[], bytes], partial(next, iterator)) + if callbacks is None: + callbacks = [] + elif callable(callbacks): + callbacks = [callbacks] + else: + callbacks = list(callbacks) + iterable_close = getattr(iterable, "close", None) + if iterable_close: + callbacks.insert(0, iterable_close) + self._callbacks = callbacks + + def __iter__(self) -> "ClosingIterator": + return self + + def __next__(self) -> bytes: + return self._next() + + def close(self) -> None: + for callback in self._callbacks: + callback() + + +def wrap_file( + environ: "WSGIEnvironment", file: t.IO[bytes], buffer_size: int = 8192 +) -> t.Iterable[bytes]: + """Wraps a file. This uses the WSGI server's file wrapper if available + or otherwise the generic :class:`FileWrapper`. + + .. versionadded:: 0.5 + + If the file wrapper from the WSGI server is used it's important to not + iterate over it from inside the application but to pass it through + unchanged. If you want to pass out a file wrapper inside a response + object you have to set :attr:`Response.direct_passthrough` to `True`. + + More information about file wrappers are available in :pep:`333`. + + :param file: a :class:`file`-like object with a :meth:`~file.read` method. + :param buffer_size: number of bytes for one iteration. + """ + return environ.get("wsgi.file_wrapper", FileWrapper)( # type: ignore + file, buffer_size + ) + + +class FileWrapper: + """This class can be used to convert a :class:`file`-like object into + an iterable. It yields `buffer_size` blocks until the file is fully + read. + + You should not use this class directly but rather use the + :func:`wrap_file` function that uses the WSGI server's file wrapper + support if it's available. + + .. versionadded:: 0.5 + + If you're using this object together with a :class:`Response` you have + to use the `direct_passthrough` mode. + + :param file: a :class:`file`-like object with a :meth:`~file.read` method. + :param buffer_size: number of bytes for one iteration. + """ + + def __init__(self, file: t.IO[bytes], buffer_size: int = 8192) -> None: + self.file = file + self.buffer_size = buffer_size + + def close(self) -> None: + if hasattr(self.file, "close"): + self.file.close() + + def seekable(self) -> bool: + if hasattr(self.file, "seekable"): + return self.file.seekable() + if hasattr(self.file, "seek"): + return True + return False + + def seek(self, *args: t.Any) -> None: + if hasattr(self.file, "seek"): + self.file.seek(*args) + + def tell(self) -> t.Optional[int]: + if hasattr(self.file, "tell"): + return self.file.tell() + return None + + def __iter__(self) -> "FileWrapper": + return self + + def __next__(self) -> bytes: + data = self.file.read(self.buffer_size) + if data: + return data + raise StopIteration() + + +class _RangeWrapper: + # private for now, but should we make it public in the future ? + + """This class can be used to convert an iterable object into + an iterable that will only yield a piece of the underlying content. + It yields blocks until the underlying stream range is fully read. + The yielded blocks will have a size that can't exceed the original + iterator defined block size, but that can be smaller. + + If you're using this object together with a :class:`Response` you have + to use the `direct_passthrough` mode. + + :param iterable: an iterable object with a :meth:`__next__` method. + :param start_byte: byte from which read will start. + :param byte_range: how many bytes to read. + """ + + def __init__( + self, + iterable: t.Union[t.Iterable[bytes], t.IO[bytes]], + start_byte: int = 0, + byte_range: t.Optional[int] = None, + ): + self.iterable = iter(iterable) + self.byte_range = byte_range + self.start_byte = start_byte + self.end_byte = None + + if byte_range is not None: + self.end_byte = start_byte + byte_range + + self.read_length = 0 + self.seekable = ( + hasattr(iterable, "seekable") and iterable.seekable() # type: ignore + ) + self.end_reached = False + + def __iter__(self) -> "_RangeWrapper": + return self + + def _next_chunk(self) -> bytes: + try: + chunk = next(self.iterable) + self.read_length += len(chunk) + return chunk + except StopIteration: + self.end_reached = True + raise + + def _first_iteration(self) -> t.Tuple[t.Optional[bytes], int]: + chunk = None + if self.seekable: + self.iterable.seek(self.start_byte) # type: ignore + self.read_length = self.iterable.tell() # type: ignore + contextual_read_length = self.read_length + else: + while self.read_length <= self.start_byte: + chunk = self._next_chunk() + if chunk is not None: + chunk = chunk[self.start_byte - self.read_length :] + contextual_read_length = self.start_byte + return chunk, contextual_read_length + + def _next(self) -> bytes: + if self.end_reached: + raise StopIteration() + chunk = None + contextual_read_length = self.read_length + if self.read_length == 0: + chunk, contextual_read_length = self._first_iteration() + if chunk is None: + chunk = self._next_chunk() + if self.end_byte is not None and self.read_length >= self.end_byte: + self.end_reached = True + return chunk[: self.end_byte - contextual_read_length] + return chunk + + def __next__(self) -> bytes: + chunk = self._next() + if chunk: + return chunk + self.end_reached = True + raise StopIteration() + + def close(self) -> None: + if hasattr(self.iterable, "close"): + self.iterable.close() # type: ignore + + +def _make_chunk_iter( + stream: t.Union[t.Iterable[bytes], t.IO[bytes]], + limit: t.Optional[int], + buffer_size: int, +) -> t.Iterator[bytes]: + """Helper for the line and chunk iter functions.""" + if isinstance(stream, (bytes, bytearray, str)): + raise TypeError( + "Passed a string or byte object instead of true iterator or stream." + ) + if not hasattr(stream, "read"): + for item in stream: + if item: + yield item + return + stream = t.cast(t.IO[bytes], stream) + if not isinstance(stream, LimitedStream) and limit is not None: + stream = t.cast(t.IO[bytes], LimitedStream(stream, limit)) + _read = stream.read + while True: + item = _read(buffer_size) + if not item: + break + yield item + + +def make_line_iter( + stream: t.Union[t.Iterable[bytes], t.IO[bytes]], + limit: t.Optional[int] = None, + buffer_size: int = 10 * 1024, + cap_at_buffer: bool = False, +) -> t.Iterator[bytes]: + """Safely iterates line-based over an input stream. If the input stream + is not a :class:`LimitedStream` the `limit` parameter is mandatory. + + This uses the stream's :meth:`~file.read` method internally as opposite + to the :meth:`~file.readline` method that is unsafe and can only be used + in violation of the WSGI specification. The same problem applies to the + `__iter__` function of the input stream which calls :meth:`~file.readline` + without arguments. + + If you need line-by-line processing it's strongly recommended to iterate + over the input stream using this helper function. + + .. versionchanged:: 0.8 + This function now ensures that the limit was reached. + + .. versionadded:: 0.9 + added support for iterators as input stream. + + .. versionadded:: 0.11.10 + added support for the `cap_at_buffer` parameter. + + :param stream: the stream or iterate to iterate over. + :param limit: the limit in bytes for the stream. (Usually + content length. Not necessary if the `stream` + is a :class:`LimitedStream`. + :param buffer_size: The optional buffer size. + :param cap_at_buffer: if this is set chunks are split if they are longer + than the buffer size. Internally this is implemented + that the buffer size might be exhausted by a factor + of two however. + """ + _iter = _make_chunk_iter(stream, limit, buffer_size) + + first_item = next(_iter, "") + if not first_item: + return + + s = _make_encode_wrapper(first_item) + empty = t.cast(bytes, s("")) + cr = t.cast(bytes, s("\r")) + lf = t.cast(bytes, s("\n")) + crlf = t.cast(bytes, s("\r\n")) + + _iter = t.cast(t.Iterator[bytes], chain((first_item,), _iter)) + + def _iter_basic_lines() -> t.Iterator[bytes]: + _join = empty.join + buffer: t.List[bytes] = [] + while True: + new_data = next(_iter, "") + if not new_data: + break + new_buf: t.List[bytes] = [] + buf_size = 0 + for item in t.cast( + t.Iterator[bytes], chain(buffer, new_data.splitlines(True)) + ): + new_buf.append(item) + buf_size += len(item) + if item and item[-1:] in crlf: + yield _join(new_buf) + new_buf = [] + elif cap_at_buffer and buf_size >= buffer_size: + rv = _join(new_buf) + while len(rv) >= buffer_size: + yield rv[:buffer_size] + rv = rv[buffer_size:] + new_buf = [rv] + buffer = new_buf + if buffer: + yield _join(buffer) + + # This hackery is necessary to merge 'foo\r' and '\n' into one item + # of 'foo\r\n' if we were unlucky and we hit a chunk boundary. + previous = empty + for item in _iter_basic_lines(): + if item == lf and previous[-1:] == cr: + previous += item + item = empty + if previous: + yield previous + previous = item + if previous: + yield previous + + +def make_chunk_iter( + stream: t.Union[t.Iterable[bytes], t.IO[bytes]], + separator: bytes, + limit: t.Optional[int] = None, + buffer_size: int = 10 * 1024, + cap_at_buffer: bool = False, +) -> t.Iterator[bytes]: + """Works like :func:`make_line_iter` but accepts a separator + which divides chunks. If you want newline based processing + you should use :func:`make_line_iter` instead as it + supports arbitrary newline markers. + + .. versionadded:: 0.8 + + .. versionadded:: 0.9 + added support for iterators as input stream. + + .. versionadded:: 0.11.10 + added support for the `cap_at_buffer` parameter. + + :param stream: the stream or iterate to iterate over. + :param separator: the separator that divides chunks. + :param limit: the limit in bytes for the stream. (Usually + content length. Not necessary if the `stream` + is otherwise already limited). + :param buffer_size: The optional buffer size. + :param cap_at_buffer: if this is set chunks are split if they are longer + than the buffer size. Internally this is implemented + that the buffer size might be exhausted by a factor + of two however. + """ + _iter = _make_chunk_iter(stream, limit, buffer_size) + + first_item = next(_iter, b"") + if not first_item: + return + + _iter = t.cast(t.Iterator[bytes], chain((first_item,), _iter)) + if isinstance(first_item, str): + separator = _to_str(separator) + _split = re.compile(f"({re.escape(separator)})").split + _join = "".join + else: + separator = _to_bytes(separator) + _split = re.compile(b"(" + re.escape(separator) + b")").split + _join = b"".join + + buffer: t.List[bytes] = [] + while True: + new_data = next(_iter, b"") + if not new_data: + break + chunks = _split(new_data) + new_buf: t.List[bytes] = [] + buf_size = 0 + for item in chain(buffer, chunks): + if item == separator: + yield _join(new_buf) + new_buf = [] + buf_size = 0 + else: + buf_size += len(item) + new_buf.append(item) + + if cap_at_buffer and buf_size >= buffer_size: + rv = _join(new_buf) + while len(rv) >= buffer_size: + yield rv[:buffer_size] + rv = rv[buffer_size:] + new_buf = [rv] + buf_size = len(rv) + + buffer = new_buf + if buffer: + yield _join(buffer) + + +class LimitedStream(io.IOBase): + """Wraps a stream so that it doesn't read more than n bytes. If the + stream is exhausted and the caller tries to get more bytes from it + :func:`on_exhausted` is called which by default returns an empty + string. The return value of that function is forwarded + to the reader function. So if it returns an empty string + :meth:`read` will return an empty string as well. + + The limit however must never be higher than what the stream can + output. Otherwise :meth:`readlines` will try to read past the + limit. + + .. admonition:: Note on WSGI compliance + + calls to :meth:`readline` and :meth:`readlines` are not + WSGI compliant because it passes a size argument to the + readline methods. Unfortunately the WSGI PEP is not safely + implementable without a size argument to :meth:`readline` + because there is no EOF marker in the stream. As a result + of that the use of :meth:`readline` is discouraged. + + For the same reason iterating over the :class:`LimitedStream` + is not portable. It internally calls :meth:`readline`. + + We strongly suggest using :meth:`read` only or using the + :func:`make_line_iter` which safely iterates line-based + over a WSGI input stream. + + :param stream: the stream to wrap. + :param limit: the limit for the stream, must not be longer than + what the string can provide if the stream does not + end with `EOF` (like `wsgi.input`) + """ + + def __init__(self, stream: t.IO[bytes], limit: int) -> None: + self._read = stream.read + self._readline = stream.readline + self._pos = 0 + self.limit = limit + + def __iter__(self) -> "LimitedStream": + return self + + @property + def is_exhausted(self) -> bool: + """If the stream is exhausted this attribute is `True`.""" + return self._pos >= self.limit + + def on_exhausted(self) -> bytes: + """This is called when the stream tries to read past the limit. + The return value of this function is returned from the reading + function. + """ + # Read null bytes from the stream so that we get the + # correct end of stream marker. + return self._read(0) + + def on_disconnect(self) -> bytes: + """What should happen if a disconnect is detected? The return + value of this function is returned from read functions in case + the client went away. By default a + :exc:`~werkzeug.exceptions.ClientDisconnected` exception is raised. + """ + from .exceptions import ClientDisconnected + + raise ClientDisconnected() + + def exhaust(self, chunk_size: int = 1024 * 64) -> None: + """Exhaust the stream. This consumes all the data left until the + limit is reached. + + :param chunk_size: the size for a chunk. It will read the chunk + until the stream is exhausted and throw away + the results. + """ + to_read = self.limit - self._pos + chunk = chunk_size + while to_read > 0: + chunk = min(to_read, chunk) + self.read(chunk) + to_read -= chunk + + def read(self, size: t.Optional[int] = None) -> bytes: + """Read `size` bytes or if size is not provided everything is read. + + :param size: the number of bytes read. + """ + if self._pos >= self.limit: + return self.on_exhausted() + if size is None or size == -1: # -1 is for consistence with file + size = self.limit + to_read = min(self.limit - self._pos, size) + try: + read = self._read(to_read) + except (OSError, ValueError): + return self.on_disconnect() + if to_read and len(read) != to_read: + return self.on_disconnect() + self._pos += len(read) + return read + + def readline(self, size: t.Optional[int] = None) -> bytes: + """Reads one line from the stream.""" + if self._pos >= self.limit: + return self.on_exhausted() + if size is None: + size = self.limit - self._pos + else: + size = min(size, self.limit - self._pos) + try: + line = self._readline(size) + except (ValueError, OSError): + return self.on_disconnect() + if size and not line: + return self.on_disconnect() + self._pos += len(line) + return line + + def readlines(self, size: t.Optional[int] = None) -> t.List[bytes]: + """Reads a file into a list of strings. It calls :meth:`readline` + until the file is read to the end. It does support the optional + `size` argument if the underlying stream supports it for + `readline`. + """ + last_pos = self._pos + result = [] + if size is not None: + end = min(self.limit, last_pos + size) + else: + end = self.limit + while True: + if size is not None: + size -= last_pos - self._pos + if self._pos >= end: + break + result.append(self.readline(size)) + if size is not None: + last_pos = self._pos + return result + + def tell(self) -> int: + """Returns the position of the stream. + + .. versionadded:: 0.9 + """ + return self._pos + + def __next__(self) -> bytes: + line = self.readline() + if not line: + raise StopIteration() + return line + + def readable(self) -> bool: + return True diff --git a/flask-app/venv/pyvenv.cfg b/flask-app/venv/pyvenv.cfg new file mode 100644 index 000000000..51b216f3b --- /dev/null +++ b/flask-app/venv/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /Library/Frameworks/Python.framework/Versions/3.8/bin +include-system-site-packages = false +version = 3.8.0 diff --git a/screenshots/.DS_Store b/screenshots/.DS_Store new file mode 100644 index 000000000..747b1b767 Binary files /dev/null and b/screenshots/.DS_Store differ diff --git a/screenshots/APM Datadog Yaml port 5050.png b/screenshots/APM Datadog Yaml port 5050.png new file mode 100755 index 000000000..34ff9273b Binary files /dev/null and b/screenshots/APM Datadog Yaml port 5050.png differ diff --git a/screenshots/APM Datadog Yaml.png b/screenshots/APM Datadog Yaml.png new file mode 100755 index 000000000..369c1e2dc Binary files /dev/null and b/screenshots/APM Datadog Yaml.png differ diff --git a/screenshots/APM To Do app on the browser.png b/screenshots/APM To Do app on the browser.png new file mode 100755 index 000000000..5408063bc Binary files /dev/null and b/screenshots/APM To Do app on the browser.png differ diff --git a/screenshots/APM To Do app showing entries.png b/screenshots/APM To Do app showing entries.png new file mode 100755 index 000000000..515f58c36 Binary files /dev/null and b/screenshots/APM To Do app showing entries.png differ diff --git a/screenshots/APM and Infrastructure Metrics Dashboard.png b/screenshots/APM and Infrastructure Metrics Dashboard.png new file mode 100644 index 000000000..cae7c5fc6 Binary files /dev/null and b/screenshots/APM and Infrastructure Metrics Dashboard.png differ diff --git a/screenshots/APM dashboard on datadog website Get results.png b/screenshots/APM dashboard on datadog website Get results.png new file mode 100755 index 000000000..33836434b Binary files /dev/null and b/screenshots/APM dashboard on datadog website Get results.png differ diff --git a/screenshots/APM datadog dashboard 1.png b/screenshots/APM datadog dashboard 1.png new file mode 100755 index 000000000..75954accb Binary files /dev/null and b/screenshots/APM datadog dashboard 1.png differ diff --git a/screenshots/APM ddtrace python app run.png b/screenshots/APM ddtrace python app run.png new file mode 100755 index 000000000..81d74d906 Binary files /dev/null and b/screenshots/APM ddtrace python app run.png differ diff --git a/screenshots/Agent Reporting Metrics.png b/screenshots/Agent Reporting Metrics.png new file mode 100755 index 000000000..1b7b64678 Binary files /dev/null and b/screenshots/Agent Reporting Metrics.png differ diff --git a/screenshots/Alert Email.png b/screenshots/Alert Email.png new file mode 100755 index 000000000..d3721705e Binary files /dev/null and b/screenshots/Alert Email.png differ diff --git a/screenshots/Configure the monitor Message.png b/screenshots/Configure the monitor Message.png new file mode 100755 index 000000000..43130b6e6 Binary files /dev/null and b/screenshots/Configure the monitor Message.png differ diff --git a/screenshots/Datadog Agent Yaml Tags 1.png b/screenshots/Datadog Agent Yaml Tags 1.png new file mode 100755 index 000000000..a2dfe72b8 Binary files /dev/null and b/screenshots/Datadog Agent Yaml Tags 1.png differ diff --git a/screenshots/Datadog Dashboard Tags.png b/screenshots/Datadog Dashboard Tags.png new file mode 100755 index 000000000..267c4224b Binary files /dev/null and b/screenshots/Datadog Dashboard Tags.png differ diff --git a/screenshots/Datadog Yaml Tags 2.png b/screenshots/Datadog Yaml Tags 2.png new file mode 100755 index 000000000..9f8062ac0 Binary files /dev/null and b/screenshots/Datadog Yaml Tags 2.png differ diff --git a/screenshots/Downtime Weekday Settings.png b/screenshots/Downtime Weekday Settings.png new file mode 100755 index 000000000..edb45a248 Binary files /dev/null and b/screenshots/Downtime Weekday Settings.png differ diff --git a/screenshots/Downtime Weekend.png b/screenshots/Downtime Weekend.png new file mode 100755 index 000000000..37567f317 Binary files /dev/null and b/screenshots/Downtime Weekend.png differ diff --git a/screenshots/Downtime monitor email on the weekend.png b/screenshots/Downtime monitor email on the weekend.png new file mode 100755 index 000000000..44814cee6 Binary files /dev/null and b/screenshots/Downtime monitor email on the weekend.png differ diff --git a/screenshots/Downtime setting dashboard.png b/screenshots/Downtime setting dashboard.png new file mode 100755 index 000000000..3a6ddf87f Binary files /dev/null and b/screenshots/Downtime setting dashboard.png differ diff --git a/screenshots/Downtime settings weekday dashboard .png b/screenshots/Downtime settings weekday dashboard .png new file mode 100755 index 000000000..982aa1b38 Binary files /dev/null and b/screenshots/Downtime settings weekday dashboard .png differ diff --git a/screenshots/Downtime settings weekend dashboard.png b/screenshots/Downtime settings weekend dashboard.png new file mode 100755 index 000000000..eb2a68587 Binary files /dev/null and b/screenshots/Downtime settings weekend dashboard.png differ diff --git a/screenshots/Email notice of snapshot anamolies.png b/screenshots/Email notice of snapshot anamolies.png new file mode 100755 index 000000000..7431dc9b1 Binary files /dev/null and b/screenshots/Email notice of snapshot anamolies.png differ diff --git a/screenshots/Metric Monitor Json.png b/screenshots/Metric Monitor Json.png new file mode 100755 index 000000000..8d8879b8f Binary files /dev/null and b/screenshots/Metric Monitor Json.png differ diff --git a/screenshots/Missing Data Email.png b/screenshots/Missing Data Email.png new file mode 100755 index 000000000..b682eec5f Binary files /dev/null and b/screenshots/Missing Data Email.png differ diff --git a/screenshots/Monitor metric Dashboard Timeboard 2.png b/screenshots/Monitor metric Dashboard Timeboard 2.png new file mode 100644 index 000000000..54ca160d6 Binary files /dev/null and b/screenshots/Monitor metric Dashboard Timeboard 2.png differ diff --git a/screenshots/Monitor metric Dashboard Timeboard.png b/screenshots/Monitor metric Dashboard Timeboard.png new file mode 100644 index 000000000..1fb577089 Binary files /dev/null and b/screenshots/Monitor metric Dashboard Timeboard.png differ diff --git a/screenshots/My Hourly Timeboard.png b/screenshots/My Hourly Timeboard.png new file mode 100755 index 000000000..e1d856e1f Binary files /dev/null and b/screenshots/My Hourly Timeboard.png differ diff --git a/screenshots/Postgres conf.d.png b/screenshots/Postgres conf.d.png new file mode 100644 index 000000000..6703d96e6 Binary files /dev/null and b/screenshots/Postgres conf.d.png differ diff --git a/screenshots/Postman API Editor.png b/screenshots/Postman API Editor.png new file mode 100755 index 000000000..2e4f8af94 Binary files /dev/null and b/screenshots/Postman API Editor.png differ diff --git a/screenshots/Warn Email.png b/screenshots/Warn Email.png new file mode 100755 index 000000000..633f89458 Binary files /dev/null and b/screenshots/Warn Email.png differ diff --git a/screenshots/mymetric Dashboard 45 Interval.png b/screenshots/mymetric Dashboard 45 Interval.png new file mode 100755 index 000000000..94f4e3602 Binary files /dev/null and b/screenshots/mymetric Dashboard 45 Interval.png differ diff --git a/screenshots/mymetric Dashboard.png b/screenshots/mymetric Dashboard.png new file mode 100755 index 000000000..7b0172014 Binary files /dev/null and b/screenshots/mymetric Dashboard.png differ diff --git a/screenshots/mymetric sum.png b/screenshots/mymetric sum.png new file mode 100755 index 000000000..38ed2704f Binary files /dev/null and b/screenshots/mymetric sum.png differ diff --git a/screenshots/mymetric yaml 45 interval.png b/screenshots/mymetric yaml 45 interval.png new file mode 100755 index 000000000..996623d12 Binary files /dev/null and b/screenshots/mymetric yaml 45 interval.png differ diff --git a/screenshots/mymetric yaml.png b/screenshots/mymetric yaml.png new file mode 100755 index 000000000..144eeaa8c Binary files /dev/null and b/screenshots/mymetric yaml.png differ diff --git a/screenshots/mymetric.py.png b/screenshots/mymetric.py.png new file mode 100755 index 000000000..f8c581491 Binary files /dev/null and b/screenshots/mymetric.py.png differ