Compare commits

..
26 Commits
Author SHA1 Message Date
Mabe 087fed017a Implement flag handling and migration status checks; refactor database test functions and update requirements 2026-09-26 16:21:44 -07:00
Mabe 220a06208a Refactor start.py to remove dotenv and db dependencies; implement new_table function for table creation and streamline admin function 2026-09-24 19:40:30 -07:00
Mabe d268cb01d5 Add check_clothe function to verify clothing existence in the database 2026-09-24 19:33:43 -07:00
Mabe 98e9b28bae Remove migration_status.flag on exit and during initialization; log any exceptions 2026-09-23 20:53:42 -07:00
Mabe 12485792d2 Add migration support with Alembic; implement migration scripts and configuration 2026-09-23 20:47:58 -07:00
Mabe c3ce9b88a2 Update .gitignore and requirements.txt; remove mariadb dependency and add SQLAlchemy 2026-09-23 19:56:12 -07:00
Mabe cfe8a0e22d Refactor database interactions to use SQLAlchemy ORM; update user and token management in api_security.py and user.py; add models for Items, Tokens, and User 2026-09-23 19:54:20 -07:00
Mabe c0108fd1e1 Add username availability check and fix cursor initialization in user.py; update token validation in api_security.py 2026-09-19 16:48:56 -07:00
Mabe c105c36086 Remove unused import of mariadb in user.py 2026-09-19 13:16:55 -07:00
Mabe f5919b2845 Refactor import paths and enhance logging in various modules; add user table creation in start.py 2026-09-19 13:13:03 -07:00
Mabe 83c288d490 Implement token management and authentication features with logging 2026-08-30 21:42:21 +02:00
Mabe f697a0aca3 Add start.py to initialize logging and create database tables 2026-08-30 17:37:36 +02:00
Mabe 1605757c80 Update .gitignore to include venv/pyvenv.cfg and venv/* directories 2026-08-29 12:31:45 +02:00
Mabe 06915f0758 Update .gitignore to include update.sh and update.ps1 files 2026-08-29 12:29:47 +02:00
Mabe 7a87b0e793 Update .gitignore to include start.sh and venv/ directories 2026-08-29 12:27:48 +02:00
Mabe 89b8b246f9 Add example environment configuration file (.env.example) 2026-08-28 21:29:24 +02:00
Mabe da1d4e9e89 Remove .env file and update .gitignore to exclude it 2026-08-28 21:27:28 +02:00
AdmMabe 5a692f9dd9 Merge pull request 'Api' (#2) from api into master
Reviewed-on: #2
2026-08-28 21:07:58 +02:00
Mabe c1931fae20 Implement CRUD operations for luggage items: add, update, delete, and fetch functionalities; enhance logging and error handling 2026-08-28 21:02:25 +02:00
Mabe 79e0af79dc Add health check API, logging setup, and environment variable support; update requirements and .gitignore 2026-08-28 18:44:29 +02:00
AdmMabe 1581538448 Update .env 2026-08-28 17:22:10 +02:00
Mabe 36cb2dc567 Add .env file for configuration, update db connection to use environment variables, and enhance requirements 2026-08-28 17:21:04 +02:00
Mabe 4dcc2b313a Remove app.log file 2026-08-26 13:07:37 +02:00
Mabe 29861506d4 venv active 2026-08-26 12:06:39 +02:00
Mabe b6737d9968 Enhance luggage management: update new_part to accept pieces, add delete functionality, and log actions 2026-08-25 21:41:21 +02:00
Mabe 95ece52457 Add LICENSE, README, and requirements files; update db.py for logging 2026-08-25 20:39:58 +02:00
603 changed files with 210633 additions and 207 deletions
+8
View File
@@ -0,0 +1,8 @@
#database
DB_NAME=
#Credentials
DB_PASSWORD=
DB_USER=
#Host Settings
DB_PORT= #standard 3306
DB_HOST=
+51
View File
@@ -0,0 +1,51 @@
start.sh
update.sh
# OS
.DS_Store
Thumbs.db
Desktop.ini
# Editors / IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# Logs
*.log
logs/
# Env / secrets
.env
.env.*
!.env.example
# Dependencies
node_modules/
vendor/
venv/
.venv/
__pycache__/
*.pyc
# Build output
dist/
build/
out/
*.egg-info/
target/
bin/
obj/
# Testing / coverage
coverage/
.nyc_output/
.pytest_cache/
# Misc
*.tmp
*.bak
*.old
.cache/
migration_status.flag
+18
View File
@@ -0,0 +1,18 @@
MIT License
Copyright (c) 2026 AdmMabe
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.
+39
View File
@@ -0,0 +1,39 @@
# Luggage
Python utility for tracking your luggage-related data in a database.
## Requirements
- Python 3.10 or newer
## Installation
Clone the repository and install the project dependencies:
```bash
git clone https://git.mabeservices.de/AdmMabe/py_luggage.git
cd luggage
```
Install dependencies if a dependency file is present:
```bash
pip install -r requirements.txt
```
## Usage
Run the application from the project directory:
```bash
python main.py
```
## Development
Keep changes focused and run the application locally before submitting a
change. Add tests alongside new functionality when applicable.
## License
See the license file included in the repository.
+152
View File
@@ -0,0 +1,152 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/migration
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to <script_location>/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# the special token `%(here)s` is available which indicates the absolute path
# to this configuration file.
#
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
# path_separator (New in Alembic 1.16.0, supersedes version_path_separator);
# This indicates what character is used to
# split lists of file paths, including version_locations and prepend_sys_path
# within configparser files such as alembic.ini.
#
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
# See notes in "escaping characters in ini files" for guidelines on
# passwords
sqlalchemy.url = driver://user:pass@localhost/dbname
# [post_write_hooks]
# This section defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner,
# against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
View File
+102
View File
@@ -0,0 +1,102 @@
from fastapi import APIRouter, HTTPException, Depends, Header
import logging
import db
import auth.api_security as auth
from pydantic import BaseModel
from api import tokens
#Logging
logging.basicConfig(
filename='api.log', # Log to a file
level=logging.DEBUG, # Log everything (DEBUG and above)
format='%(asctime)s %(levelname)s %(message)s'
)
logging.info('Application started')
try:
logging.debug('Debug info here')
except Exception as e:
logging.exception('An error occurred')
router = APIRouter(prefix="/db", tags=["db"])
@router.get("/help")
def get_help():
return {"name of clothes": "example",
"weight": "sample weight (only floats)",
"pieces": "sample pieces (only ints)"}
class Item_name(BaseModel):
name_of_clothes: str
class Config:
fields = {
"name_of_clothes": "name of clothes"
}
class Item_weight(BaseModel):
weight: float
class Item_piece(BaseModel):
pieces: int
class Item_duration(BaseModel):
duration: str
@router.post("/insert", status_code=201)
def insert(item_name: Item_name, item_weight: Item_weight, item_pieces: Item_piece, token: str = Depends(tokens.check_token)):
name = item_name.name_of_clothes.upper()
weight = item_weight.weight
pieces = item_pieces.pieces
db.new_part(name, weight, pieces)
logging.info("new Item created in DB")
return {"message": "Item inserted"}
@router.put("/update/{clothe_name}/pieces", status_code=202)
def update_pieces(clothe_name: str, item_pieces: Item_piece, token: str = Depends(tokens.check_token)):
name = clothe_name.upper()
pieces = item_pieces.pieces
if db.check_clothe(name) != None:
db.update_pieces(name, pieces)
logging.info(f"Item {name} was set a new count")
return {"message": "pieces updated"}
else:
logging.error(f"Item {name} not found")
raise HTTPException(status_code=404, detail=f"Item {name} not found")
@router.put("/update/{clothe_name}/weight", status_code=202)
def update_weight(clothe_name: str, item: Item_weight, token: str = Depends(tokens.check_token)):
name = clothe_name.upper()
weight = item.weight
if db.check_clothe(name) != None:
db.update_weight(name, weight)
logging.info(f"Item {name} was set a new weight")
return {"message": "weight updated"}
else:
logging.error(f"Item {name} not found")
raise HTTPException(status_code=404, detail=f"Item {name} not found")
@router.delete("/{clothe_name}/delete", status_code=204)
def delete(clothe_name: str, token: str = Depends(tokens.check_token)):
name = clothe_name.upper()
if db.check_clothe(name) != None:
db.delete(name)
logging.critical(f"Item {name} was deleted")
return {"message": "Item was deleted"}
else:
logging.error(f"Item {name} not found")
raise HTTPException(status_code=404, detail=f"Item {name} not found")
+26
View File
@@ -0,0 +1,26 @@
from fastapi import APIRouter
import logging
#Logging
logging.basicConfig(
filename='api.log', # Log to a file
level=logging.DEBUG, # Log everything (DEBUG and above)
format='%(asctime)s %(levelname)s %(message)s'
)
logging.info('Application started')
try:
logging.debug('Debug info here')
except Exception as e:
logging.exception('An error occurred')
router = APIRouter(prefix="/health", tags=["health"])
@router.get("/")
def health_check():
logging.info("get health check request")
return {"status": "ok"}
+75
View File
@@ -0,0 +1,75 @@
from fastapi import APIRouter, HTTPException
import logging
import db
from pydantic import BaseModel
#Logging
logging.basicConfig(
filename='api.log', # Log to a file
level=logging.DEBUG, # Log everything (DEBUG and above)
format='%(asctime)s %(levelname)s %(message)s'
)
logging.info('Application started')
try:
logging.debug('Debug info here')
except Exception as e:
logging.exception('An error occurred')
router = APIRouter(prefix="/query", tags=["query"])
@router.get("/help", status_code=200)
def get_help():
return {"name of clothes": "example"}
@router.get("/{clothe_name}/weight", status_code=206)
def get_weight(clothe_name: str):
name = clothe_name.upper()
if db.check_clothe(name) != None:
weight = db.fetch_weight(name)
return {"weight": weight}
else:
logging.error(f"Item {name} not found")
raise HTTPException(status_code=404, detail=f"Item {name} not found")
@router.get("/weight", status_code=200)
def get_luggage_weight():
weight = db.fetch_luggage_weight()
return {"weight": weight}
@router.get("/{clothe_name}/pieces",status_code=206)
def get_pieces(clothe_name: str):
name = clothe_name.upper()
if db.check_clothe(name) != None:
pieces = db.fetch_piece(name)
return {"pieces": pieces}
else:
logging.error(f"Item {name} not found")
raise HTTPException(status_code=404, detail=f"Item {name} not found")
@router.get("/pieces", status_code=200)
def get_all_pieces():
pieces = db.fetch_all_pieces()
return {"pieces": pieces}
@router.get("/clothes")
def get_clothes():
clothes = db.fetch_clothes()
return {"clothes": clothes}
@router.get("/{clothe_name}/all", status_code=102)
def get_all(clothe_name: str):
name = clothe_name.upper()
if db.check_clothe(name) != None:
weight = db.fetch_weight(name)
pieces = db.fetch_piece(name)
return{
"clothe": name,
"weight": weight,
"pieces": pieces
}
else:
logging.error(f"Item {name} not found")
raise HTTPException(status_code=404, detail=f"Item {name} not found")
+60
View File
@@ -0,0 +1,60 @@
from fastapi import APIRouter, HTTPException, Depends, Header
import logging
import db
import auth.api_security as auth
from pydantic import BaseModel
def check_token(token:str = Header(...)):
if not auth.check_token(token):
raise HTTPException(status_code=401, detail="Invalid token")
return token
def check_admin_token(admin_token: str = Header(...)):
if not auth.check_admin_token(admin_token):
raise HTTPException(status_code=401, detail="No admin token")
return admin_token
#Logging
logging.basicConfig(
filename='api.log', # Log to a file
level=logging.DEBUG, # Log everything (DEBUG and above)
format='%(asctime)s %(levelname)s %(message)s'
)
logging.info('Application started')
try:
logging.debug('Debug info here')
except Exception as e:
logging.exception('An error occurred')
router = APIRouter(prefix="/tokens", tags=["tokens"])
class Item_duration(BaseModel):
duration: str | int
class Item_token(BaseModel):
token: str
@router.get("/help")
def help():
return{"duration": "string or int"}
@router.post("/create")
def create_token(duration: Item_duration, admin_token: str = Depends(check_admin_token)):
new_token = auth.generate_token(duration.duration)
return {"new token": new_token}
@router.delete("/delete")
def delete(token: Item_token, admin_token: str = Depends(check_admin_token)):
auth.delete_token(token.token)
return {"messages": "token deleted"}
@router.get("/test")
def test(token: str = Depends(check_token)):
return {"message": "you can authenticate", "status": "ok"}
+8
View File
@@ -0,0 +1,8 @@
from fastapi import FastAPI
from api import health, db, query, tokens
app = FastAPI()
app.include_router(health.router)
app.include_router(db.router)
app.include_router(query.router)
app.include_router(tokens.router)
+122
View File
@@ -0,0 +1,122 @@
import time
from dotenv import load_dotenv
import db
import os
from datetime import datetime, timedelta
import logging
from argon2 import PasswordHasher
import random
import string
from models import Tokens
#Logging
logging.basicConfig(
filename='api.log', # Log to a file
level=logging.DEBUG, # Log everything (DEBUG and above)
format='%(asctime)s %(levelname)s %(message)s'
)
logging.info('Application started')
try:
logging.debug('Debug info here')
except Exception as e:
logging.exception('An error occurred')
session = db.session
def generate_token(duration):
ph = PasswordHasher()
token = "".join(random.choices(string.ascii_lowercase + string.digits, k=32))
print(token)
hash = ph.hash(token)
end_date = enddate(duration)
time = datetime.now()
new_item = Tokens(tokens=hash, time=time, end_date=end_date)
session.add(new_item)
session.commit()
logging.info(f"Token till {end_date} created")
return token
def enddate(duration):
if duration == "week":
return datetime.now() + timedelta(days=7)
if duration == "day":
return datetime.now() + timedelta(days=1)
if duration == "year":
return datetime.now() + timedelta(days=365)
if isinstance(duration, int):
return datetime.now() + timedelta(days=duration)
if duration == "forever":
return datetime.now() + timedelta(days=4000)
raise ValueError(f"Invalid duration: {duration}")
def check_token(token):
ph = PasswordHasher()
valid = False
hashes = [row[0] for row in session.query(Tokens.tokens).all()]
for hash in hashes:
try:
valid = ph.verify(hash, token)
if valid == True:
return valid
except Exception as e:
logging.error("Token not the same")
logging.error(e)
return valid
def check_validaty():
end_dates = [row[0] for row in session.query(Tokens.end_date).all()]
for end_date in end_dates:
end_date_dt = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
if datetime.now() > end_date_dt:
session.query(Tokens).filter_by(end_date=end_date).delete()
session.commit()
def admin_token():
ph = PasswordHasher()
token = "".join(random.choices(string.ascii_lowercase + string.digits, k=64))
hash = ph.hash(token)
time = datetime.now()
end_date = enddate("forever")
new_item = Tokens(tokens=hash, time=time, end_date=end_date)
session.add(new_item)
session.commit()
logging.info(f"Token till {end_date} created")
return token
def check_admin_token(token):
valid = False
if len(token) == 64:
ph = PasswordHasher()
cursor = db.connection.cursor()
hashes = [row[0] for row in session.query(Tokens.tokens).all()]
for hash in hashes:
try:
valid = ph.verify(hash, token)
if valid == True:
return valid
except Exception as e:
logging.info(e)
def delete_token(token):
if not len(token) == 64:
ph = PasswordHasher()
hashes = [row[0] for row in session.query(Tokens.tokens).all()]
for hash in hashes:
try:
valid = ph.verify(hash, token)
if valid == True:
session.query(Tokens).filter_by(tokens=hash).delete()
db.connection.commit()
logging.critical("token deleted")
except Exception as e:
logging.error(e)
+65
View File
@@ -0,0 +1,65 @@
from argon2 import PasswordHasher
import db
from models import User
import logging
#Logging
logging.basicConfig(
filename='auth/user.log', # Log to a file
level=logging.DEBUG, # Log everything (DEBUG and above)
format='%(asctime)s %(levelname)s %(message)s'
)
logging.info('Application started')
try:
logging.debug('Debug info here')
except Exception as e:
logging.exception('An error occurred')
session = db.session
def create_user(username, password, role):
user = username
passwd = PasswordHasher().hash(password)
new_item = User(username=user, password=passwd, role=role)
def check_login(username, password):
result = session.query(User.password).filter_by(username=username).first()
hash_passwd = result[0] if result else None
valid = False
try:
valid = PasswordHasher().verify(hash_passwd, password)
if valid == True:
return valid
except Exception as e:
logging.error(e)
return False
def delete_user(username, password):
result = session.query(User.password).filter_by(username=username).first()
hash_password = result[0] if result else None
valid = False
try:
valid = PasswordHasher().verify(hash_password, password)
except Exception as e:
logging.error(e)
if valid == True:
try:
session.query(User).filter_by(username=username).delete()
session.commit()
logging.info(f"User {username} was successfully deleted")
except Exception as e:
logging.error(e)
def check_username(username):
try:
exists = session.query(User).filter_by(username=username).first() is not None
return exists # True if taken, False if available
except db.IntegrityError as e:
if e.errno == 1062:
logging.error("Username taken")
return True #If the username is not available it returns True
def get_role(username):
result = session.query(User.role).filter_by(username=username).first()
return result[0] if result else None
+194
View File
@@ -0,0 +1,194 @@
import db
import logging
import time
import os
import sys
from flags import status
print("loading")
#Logging
logging.basicConfig(
filename='app.log', # Log to a file
level=logging.DEBUG, # Log everything (DEBUG and above)
format='%(asctime)s %(levelname)s %(message)s'
)
logging.info('Application started')
try:
logging.debug('Debug info here')
except Exception as e:
logging.exception('An error occurred')
def insert():
clothe = input("How is the clothe called? ").upper()
weight = float(input("The weight please? "))
pieces = int(input("How many pieces? ints only "))
db.new_part(clothe, weight, pieces)
print("it is inserted")
time.sleep(3)
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def insert():
clothe = input("How is the clothe called? ").upper()
weight = float(input("The weight please? "))
pieces = int(input("How many pieces? ints only "))
db.new_part(clothe, weight, pieces)
print("it is inserted")
time.sleep(3)
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def update():
clothe = input("How is the clothe called? ").upper()
weight = input("The weight please? ")
pieces = input("How many pieces? ints only ")
db.update_weight(clothe, weight)
db.update_pieces(clothe, pieces)
print("it is updated")
time.sleep(3)
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def get_weight():
clothe = input("How is the clothe called? ").upper()
weight = db.fetch_weight(clothe)
print(weight)
time.sleep(3)
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def get_luggage_weight():
weight = db.fetch_luggage_weight()
print(f"The luggage has a weight of {weight}g")
time.sleep(3)
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def get_pieces():
clothe = input("How is the clothe called? ").upper()
pieces = db.fetch_piece(clothe)
print(f"You have {pieces} of {clothe}")
time.sleep(3)
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def get_all_pieces():
pieces = db.fetch_all_pieces()
print(f"You have {pieces} in your luggage")
time.sleep(3)
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def get_clothes():
clothes = db.fetch_clothes()
for clothe in clothes:
print(clothe)
time.sleep(3)
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def get_weight_pieces():
clothe = input("How is the clothe called? ").upper()
weight = db.fetch_weight(clothe)
pieces = db.fetch_piece(clothe)
print(f"You have {pieces} of {clothe}. The weight is {weight}")
time.sleep(3)
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def delete():
clothe = input("How is the clothe called? ").upper()
really = input("Do you really want to delete it? Y/N").upper()
if really == "Y":
db.delete(clothe)
print(f"{clothe} was deleted")
time.sleep(3)
else:
time.sleep(0.5)
logging.info("no item deleted")
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def exit_programm():
time.sleep(0.5)
print(r"""
## ## ##########
### ### ## ##
#### #### ## ##
## #### ## ##########
## ## ## ## ##
## ## ## ##
""")
time.sleep(0.5)
print("")
print("")
print("Disconnect DB")
print("logout")
print("shutting down")
time.sleep(0.75)
os.system('cls' if os.name == 'nt' else 'clear')
sys.exit()
os._exit(1)
def menu():
print("Menu")
print("1: Insert Item")
print("2: Update Item")
print("3: Get weight of clothe")
print("4: Get weight of luggage")
print("5: Get pieces of clothe")
print("6: Get pieces of luggage")
print("7: Get clothes")
print("8: Get weight and pieces of clothes")
print("9: Delete Item")
print("0: EXIT")
choice = input("Type number here ")
os.system('cls' if os.name == 'nt' else 'clear')
logging.info(f"{choice} is choosen")
time.sleep(0.5)
if choice == "1":
insert()
if choice == "2":
update()
if choice == "3":
get_weight()
if choice == "4":
get_luggage_weight()
if choice == "5":
get_pieces()
if choice == "6":
get_all_pieces()
if choice == "7":
get_clothes()
if choice == "8":
get_weight_pieces()
if choice == "0":
exit_programm()
if choice == "9":
delete()
#The Terminal App Starts here
time.sleep(2)
print(r"""
## ## ##########
### ### ## ##
#### #### ## ##
## #### ## ##########
## ## ## ## ##
## ## ## ##
""")
while not status.get_migration():
time.sleep(0.5)
logging.info("Menu is starting")
time.sleep(2.5)
menu()
+65 -63
View File
@@ -1,8 +1,10 @@
import mariadb
import os
import logging import logging
from dotenv import load_dotenv
import os
from models import Items
from sqlalchemy import create_engine, func
from sqlalchemy.orm import sessionmaker
from urllib.parse import quote_plus
#Logging #Logging
@@ -18,83 +20,83 @@ try:
except Exception as e: except Exception as e:
logging.exception('An error occurred') logging.exception('An error occurred')
#using .env
load_dotenv()
db_name = os.getenv("DB_NAME")
db_psswd = os.getenv("DB_PASSWORD")
db_user = os.getenv("DB_USER")
db_port = os.getenv("DB_PORT") or "3306"
db_host = os.getenv("DB_HOST")
if db_host == None:
logging.error("no hostname provided")
os._exit(1)
def connect(): engine = create_engine(f"mariadb+mariadbconnector://{db_user}:{quote_plus(db_psswd)}@{db_host}:{db_port}/{db_name}")
try: Session = sessionmaker(bind=engine)
global connection session = Session()
connection = mariadb.connect(user="luggage",
password="13579,QsC",
host="192.168.1.74",
port=3307,
database="canada_luggage")
print("Connected to MariaDB successfully!")
logging.info("Connected to MariaDB successfully!")
except mariadb.Error as e:
print(f"Error connecting to DB: {e}")
logging.error("Error connecting")
logging.debug(f"Error connecting to DB: {e}")
def new_part(cloth_type, weigth, pieces): def new_part(cloth_type, weight, pieces):
existing = session.query(Items).filter_by(clothes=cloth_type).first()
if existing:
logging.debug("Item already inserted")
else:
if type(pieces) is not int: if type(pieces) is not int:
print("only ints allowed") print("only ints allowed")
logging.error("only ints allowed") logging.error("only ints allowed")
else: else:
cursor = connection.cursor() new_item = Items(clothes=cloth_type, weight=weight, pieces=pieces)
executed = "INSERT INTO luggage (clothes, weigth, pieces) VALUES (?, ?)" session.add(new_item)
executed_data = (cloth_type, weigth, pieces) session.commit()
cursor.execute(executed, executed_data)
connection.commit()
logging.info("new Insert is in DB") logging.info("new Insert is in DB")
def fetch_clothes(): def update_weight(cloth_type, weight):
cursor = connection.cursor() item = session.query(Items).filter_by(clothes=cloth_type).first()
cursor.execute("SELECT clothes FROM luggage") item.weight = weight
clothes = [str(row[0]) for row in cursor.fetchall()] session.commit()
logging.info("fetched clothes") logging.info("weight updated")
return clothes
def update_weigth(cloth_type, weigth):
cursor = connection.cursor()
executed = "UPDATE luggage SET weigth = ? WHERE clothes = ?"
executed_data = (weigth, cloth_type)
cursor.execute(executed, executed_data)
connection.commit()
logging.info("weigth updated")
def update_pieces(cloth_type, pieces): def update_pieces(cloth_type, pieces):
if type(pieces) is not int: if type(pieces) is not int:
logging.error("only ints allowed") logging.error("only ints allowed")
else: else:
cursor = connection.cursor() item = session.query(Items).filter_by(clothes=cloth_type).first()
executed = "UPDATE luggage SET pieces = ? WHERE clothes = ?" item.pieces = pieces
executed_data = (pieces, cloth_type) session.commit()
cursor.execute(executed, executed_data)
connection.commit()
logging.info("pieces updated") logging.info("pieces updated")
def fetch_weigth(cloth_type): def delete(cloth_type):
cursor = connection.cursor() logging.critical(f"{cloth_type} was deleted")
cursor.execute("SELECT weigth FROM luggage WHERE clothes = ?", (cloth_type,)) session.query(Items).filter_by(clothes=cloth_type).delete()
weigth = cursor.fetchone()[0] session.commit()
logging.info("fetched weigth")
return weigth
def fetch_luggage_weigth():
cursor = connection.cursor() def fetch_clothes():
cursor.execute("SELECT sum(weigth) FROM luggage") logging.info("fetched clothes")
weigth = cursor.fetchone()[0] return [row[0] for row in session.query(Items.clothes).all()]
logging.info("fetched luggage weigth")
return weigth def fetch_weight(cloth_type):
logging.info("fetched weight")
result = session.query(Items.weight).filter_by(clothes=cloth_type).first()
return result[0] if result else None
def fetch_luggage_weight():
logging.info("fetched whole weight")
result = session.query(func.sum(Items.weight)).scalar()
return result if result else None
def fetch_piece(cloth_type): def fetch_piece(cloth_type):
cursor = connection.cursor() logging.info("fetched pieces")
cursor.execute("SELECT pieces FROM luggage WHERE clothes = ?", (cloth_type,)) result = session.query(Items.pieces).filter_by(clothes=cloth_type).first()
pieces = cursor.fetchone()[0] return result[0] if result else None
logging.info("fetched luggage pieces")
return pieces
def fetch_all_pieces(): def fetch_all_pieces():
cursor = connection.cursor() logging.info("fetched all pieces")
cursor.execute("SELECT sum(pieces) FROM luggage") result = session.query(Items.pieces).all()
pieces = cursor.fetchone()[0] return [row[0] for row in session.query(Items.pieces).all()]
#Checks if clothe is in db
def check_clothe(clothe_type):
exists = session.query(Items.clothes).filter_by(clothes=clothe_type).first() is not None
return exists
View File
+50
View File
@@ -0,0 +1,50 @@
import threading
from watchdog.events import PatternMatchingEventHandler
from watchdog.observers import Observer
import os
import time
FLAGS_DIR = os.path.dirname(os.path.abspath(__file__))
class FlagHandler(PatternMatchingEventHandler):
def __init__(self, shared_state, lock):
super().__init__(patterns=["*.flag"], ignore_directories=True)
self.shared_state = shared_state
self.lock = lock
def on_created(self, event):
try:
with open(event.src_path, "r") as f:
content = f.read().strip()
except FileNotFoundError:
return
try:
os.remove(event.src_path)
except FileNotFoundError:
pass
with self.lock:
self.shared_state["current"] = content
time.sleep(2)
shared_state = {"current": None}
lock = threading.Lock()
observer = Observer()
observer.schedule(FlagHandler(shared_state, lock), path=FLAGS_DIR, recursive=False)
observer.start()
def get_status():
with lock:
current = shared_state["current"]
if current == "db_check_done":
return False
return current
def get_migration():
with lock:
current = shared_state["current"]
if current == "migration_done":
return True
return False
+66 -139
View File
@@ -1,12 +1,18 @@
import db
import logging import logging
import time import subprocess
import os
import sys import sys
import os
import auth.api_security as auth
import time
import threading
from tests import db_test
print("loading") try:
open("app.log", "w").close() os.remove("api.log")
db.connect() os.remove("api_server.log")
os.remove("app.log")
except Exception as e:
logging.error(e)
#Logging #Logging
logging.basicConfig( logging.basicConfig(
filename='app.log', # Log to a file filename='app.log', # Log to a file
@@ -20,142 +26,63 @@ try:
except Exception as e: except Exception as e:
logging.exception('An error occurred') logging.exception('An error occurred')
def insert():
clothe = input("How is the clothe called? ").upper()
weigth = input("The weigth please? ")
pieces = input("How many pieces? ints only ")
db.new_part(clothe, weigth, pieces)
print("it is inserted")
time.sleep(3)
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def update(): env = os.environ.copy()
clothe = input("How is the clothe called? ").upper() env["PYTHONIOENCODING"] = "utf-8"
weigth = input("The weigth please? ")
pieces = input("How many pieces? ints only ")
db.update_weigth(clothe, weigth)
db.update_pieces(clothe, pieces)
print("it is updated")
time.sleep(3)
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def get_weigth(): try:
clothe = input("How is the clothe called? ").upper() os.remove("migration_status.flag")
weigth = db.fetch_weigth(clothe) except Exception as e:
print(weigth) logging.info(e)
time.sleep(3) #migration
os.system('cls' if os.name == 'nt' else 'clear') def run_migration():
menu() global migration_done
process = subprocess.Popen(
def get_luggage_weigth(): ["python", "migration/alembic_scipt.py"],
weigth = db.fetch_luggage_weigth() stdout=subprocess.DEVNULL,
print(f"The luggage has a weigth of {weigth}kg") stderr=subprocess.DEVNULL
time.sleep(3) )
os.system('cls' if os.name == 'nt' else 'clear') process.wait()
menu() migration_done = (process.returncode == 0)
with open("flags/migration_status.flag", "w") as f:
def get_pieces(): f.write("migration_done" if migration_done else "failed")
clothe = input("How is the clothe called? ").upper()
pieces = db.fetch_piece(clothe)
print(f"You have {pieces} of {clothe}")
time.sleep(3)
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def get_all_pieces():
pieces = db.fetch_all_pieces()
print(f"You have {pieces} in your luggage")
time.sleep(3)
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def get_clothes():
clothes = db.fetch_clothes()
for clothe in clothes:
print(clothe)
time.sleep(3)
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def get_weigth_pieces():
clothe = input("How is the clothe called? ").upper()
weigth = db.fetch_weigth(clothe)
pieces = db.fetch_piece(clothe)
print(f"You have {pieces} of {clothe}. The weigth is {weigth}")
time.sleep(3)
os.system('cls' if os.name == 'nt' else 'clear')
menu()
def exit_programm():
time.sleep(0.5)
print(r"""
## ## ##########
### ### ## ##
#### #### ## ##
## #### ## ##########
## ## ## ## ##
## ## ## ##
""")
time.sleep(0.5)
print("")
print("")
print("Disconnect DB")
print("logout")
print("shutting down")
time.sleep(0.75)
os.system('cls' if os.name == 'nt' else 'clear')
sys.exit()
def menu():
print("Menu")
print("1: Insert Item")
print("2: Update Item")
print("3: Get weigth of clothe")
print("4: Get weigth of luggage")
print("5: Get pieces of clothe")
print("6: Get pieces of luggage")
print("7: Get clothes")
print("8: Get weigth and pieces of clothes")
print("0: EXIT")
choice = input("Type number here ")
os.system('cls' if os.name == 'nt' else 'clear')
logging.info(f"{choice} is choosen")
time.sleep(0.5)
if choice == "1":
insert()
if choice == "2":
update()
if choice == "3":
get_weigth()
if choice == "4":
get_luggage_weigth()
if choice == "5":
get_pieces()
if choice == "6":
get_all_pieces()
if choice == "7":
get_clothes()
if choice == "8":
get_weigth_pieces()
if choice == "0":
exit_programm()
#DB test
def run_db_test():
#The Terminal App Starts here tokens = db_test.test_table("tokens", ["tokens", "time", "end_date"])
user = db_test.test_table("user", ["username", "password", "role"])
items = db_test.test_table("luggage", ["clothes", "weight", "pieces"])
time.sleep(5) time.sleep(5)
print(r""" if tokens == True & user == True & items == True:
## ## ########## with open("flags/db.flag", "w") as f:
### ### ## ## f.write("db_check_done")
#### #### ## ## else:
## #### ## ########## os._exit(1)
## ## ## ## ##
## ## ## ## #cli and api
""") log_file = open("api_server.log", "w", encoding="utf-8")
logging.info("Menu is starting") api_process = subprocess.Popen(
time.sleep(2.5) ["fastapi", "dev", "api_server.py", "--reload-dir", "web", "--reload-dir", "api", "--reload-dir", "api_server.py",],
menu() stdout=log_file,
stderr=log_file,
env=env
)
cli_process = subprocess.Popen([sys.executable, "cli.py"])
db_thread = threading.Thread(target=run_db_test)
db_thread.start()
thread = threading.Thread(target=run_migration)
thread.start()
threading.Thread(target=api_process.wait, daemon=True).start()
threading.Thread(target=cli_process.wait, daemon=True).start()
# automatic durataion validation check
while True:
time.sleep(300)
try:
auth.check_validaty()
except Exception as e:
logging.info(e)
+1
View File
@@ -0,0 +1 @@
Generic single-database configuration.
+67
View File
@@ -0,0 +1,67 @@
import os
from alembic.config import Config
from alembic.script import ScriptDirectory
from alembic.runtime.migration import MigrationContext
from alembic import command
from sqlalchemy import create_engine
from alembic import command
from dotenv import load_dotenv
import logging
#Logging
logging.basicConfig(
filename='app.log', # Log to a file
level=logging.DEBUG, # Log everything (DEBUG and above)
format='%(asctime)s %(levelname)s %(message)s'
)
logging.info('Application started')
try:
logging.debug('Debug info here')
except Exception as e:
logging.exception('An error occurred')
load_dotenv()
db_name = os.getenv("DB_NAME")
db_psswd = os.getenv("DB_PASSWORD")
db_user = os.getenv("DB_USER")
db_port = os.getenv("DB_PORT") or "3306"
db_host = os.getenv("DB_HOST")
if db_host == None:
os._exit(1)
engine_url = f"mariadb+mariadbconnector://{db_user}:{db_psswd}@{db_host}:{db_port}/{db_name}"
base_dir = os.path.dirname(__file__)
ini_path = os.path.join(base_dir, "..", "alembic.ini")
alembic_cfg = Config(ini_path)
alembic_cfg.set_section_option(alembic_cfg.config_ini_section, "sqlalchemy.url", engine_url)
script = ScriptDirectory.from_config(alembic_cfg)
engine = create_engine(engine_url)
def check_db():
with engine.connect() as conn:
context = MigrationContext.configure(conn)
current_rev = context.get_current_revision()
head_rev = script.get_current_head()
if current_rev != head_rev:
command.upgrade(alembic_cfg, "head")
return False
def auto_lookup():
command.revision(alembic_cfg, message="add tables and rows", autogenerate=True)
script = ScriptDirectory.from_config(alembic_cfg)
head_rev = script.get_revision(script.get_current_head())
with open(head_rev.path) as f:
content = f.read()
if "pass\n # ### end Alembic commands ###" in content:
os.remove(head_rev.path)
command.stamp(alembic_cfg, "head")
logging.info("No changes DB detected")
else:
command.upgrade(alembic_cfg, "head")
logging.info("New DB changes, upgrade table")
auto_lookup()
+84
View File
@@ -0,0 +1,84 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from models import Base
metadata = Base.metadata
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
if config.config_file_name is not None:
fileConfig(config.config_file_name)
+28
View File
@@ -0,0 +1,28 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}
+1
View File
@@ -0,0 +1 @@
from .models import Items, Tokens, User, Base
+23
View File
@@ -0,0 +1,23 @@
from sqlalchemy.orm import declarative_base
from sqlalchemy import Column, String, Numeric, Integer
Base = declarative_base()
class Tokens(Base):
__tablename__ = "tokens"
tokens = Column(String(255), primary_key=True)
time = Column(String(255))
end_date = Column(String(255))
class Items(Base):
__tablename__ = "luggage"
clothes = Column(String(50), primary_key=True)
weight = Column(Numeric(20, 2))
pieces = Column(Integer)
class User(Base):
__tablename__ = "user"
username = Column(String(50), primary_key=True)
password = Column(String(255))
role = Column(String(50))
+10
View File
@@ -0,0 +1,10 @@
python-dotenv == 1.2.3
fastapi[standard] == 0.141.1
argon2-cffi == 25.1.0
Jinja2==3.1.6
MarkupSafe==3.0.3
itsdangerous==2.2.0
alembic==1.20.0
SQLAlchemy==2.0.54
watchdog==6.0.0
#watchfiles==1.2.0
+36
View File
@@ -0,0 +1,36 @@
from migration.alembic_scipt import auto_lookup
import time
import logging
import auth.api_security as auth
import os
#Logging
logging.basicConfig(
filename='app.log', # Log to a file
level=logging.DEBUG, # Log everything (DEBUG and above)
format='%(asctime)s %(levelname)s %(message)s'
)
logging.info('Application started')
try:
logging.debug('Debug info here')
except Exception as e:
logging.exception('An error occurred')
def new_table():
auto_lookup()
def admin():
token = auth.admin_token()
print(token)
logging.info(token)
time.sleep(300)
os.remove("app.log")
os._exit(1)
new_table()
admin()
+46
View File
@@ -0,0 +1,46 @@
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import os
import db
import logging
import sys
from migration import alembic_scipt as alembic
from sqlalchemy import inspect
session = db.session
engine = db.engine
def test_column(engine, tablename, row):
try:
inspector = inspect(engine)
columns = [col["name"] for col in inspector.get_columns(tablename)]
return row in columns
except Exception as e:
logging.error(e)
def test_table(table, columns):
for column in columns:
try:
exists = test_column(engine, table, column)
if exists == False:
logging.error(f"DB has not all columns. {column} is missing")
print(f"Column {column} is missing")
sys.exit()
os._exit(1)
return False
else:
logging.info("Column fine")
except Exception as e:
logging.error(e)
return True
def migration_check():
if alembic.check_db() == False:
print("migrating")
logging.info("DB is migrating")
+247
View File
@@ -0,0 +1,247 @@
<#
.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.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
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 VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# 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("\\/")
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 virtual 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
}
$env:VIRTUAL_ENV_PROMPT = $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"
+70
View File
@@ -0,0 +1,70 @@
# 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
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
# on Windows, a path can contain colons and backslashes and has to be converted:
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
# transform D:\path\to\venv to /d/path/to/venv on MSYS
# and to /cygdrive/d/path/to/venv on Cygwin
export VIRTUAL_ENV=$(cygpath /home/jakob/Documents/py_luggage/venv)
else
# use the path as-is
export VIRTUAL_ENV=/home/jakob/Documents/py_luggage/venv
fi
_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:-}"
PS1='(venv) '"${PS1:-}"
export PS1
VIRTUAL_ENV_PROMPT='(venv) '
export VIRTUAL_ENV_PROMPT
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null
+27
View File
@@ -0,0 +1,27 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
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; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV /home/jakob/Documents/py_luggage/venv
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = '(venv) '"$prompt"
setenv VIRTUAL_ENV_PROMPT '(venv) '
endif
alias pydoc python -m pydoc
rehash
+69
View File
@@ -0,0 +1,69 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/). You cannot run it directly.
function deactivate -d "Exit virtual environment 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"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV /home/jakob/Documents/py_luggage/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
# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
# Restore the return status of the previous command.
echo "exit $old_status" | .
# Output the original/"old" prompt.
_old_fish_prompt
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
set -gx VIRTUAL_ENV_PROMPT '(venv) '
end
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/home/jakob/Documents/py_luggage/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/home/jakob/Documents/py_luggage/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/home/jakob/Documents/py_luggage/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+1
View File
@@ -0,0 +1 @@
python3
+1
View File
@@ -0,0 +1 @@
/usr/bin/python3
+1
View File
@@ -0,0 +1 @@
python3
@@ -0,0 +1,86 @@
Metadata-Version: 2.4
Name: mariadb
Version: 1.1.14
Summary: Python MariaDB extension
Home-page: https://www.github.com/mariadb-corporation/mariadb-connector-python
Author: Georg Richter
License: LGPL 2.1
Project-URL: Bug Tracker, https://jira.mariadb.org/
Project-URL: Documentation, https://mariadb-corporation.github.io/mariadb-connector-python/
Project-URL: Source Code, https://www.github.com/mariadb-corporation/mariadb-connector-python
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Environment :: MacOS X
Classifier: Environment :: Win32 (MS Windows)
Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)
Classifier: Programming Language :: C
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX
Classifier: Intended Audience :: End Users/Desktop
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Topic :: Database
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: packaging
Dynamic: author
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: license-file
Dynamic: project-url
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary
<p align="center">
<a href="http://mariadb.com/">
<img src="https://mariadb.com/kb/static/images/logo-2018-black.png">
</a>
</p>
# MariaDB Connector/Python
[![License (LGPL version 2.1)][licence-image]](LICENSE)
[![Python 3.7][python-image]][python-url]
[![Build Status](https://travis-ci.com/mariadb-corporation/mariadb-connector-python.svg?branch=1.1)](https://app.travis-ci.com/mariadb-corporation/mariadb-connector-python)
<a href="https://scan.coverity.com/projects/mariadb-connector-python">
<img alt="Coverity Scan Build Status"
src="https://scan.coverity.com/projects/21386/badge.svg"/>
</a>
MariaDB Connector/Python enables python programs to access MariaDB and MySQL databases, using an API
which is compliant with the Python DB API 2.0 (PEP-249). It is written in C and uses MariaDB Connector/C
client library for client server communication.
## License
MariaDB Connector/Python is licensed under the LGPL 2.1 or later (LGPL-2.1-or-later)
## Source code
MariaDB Connector/Python source code is hosted on [Github](https://github.com/mariadb-corporation/mariadb-connector-python)
## Documentation
MariaDB Connector/Python documentation can be found on [Github Pages](https://mariadb-corporation.github.io/mariadb-connector-python/)
## Bugs
Bugs and feature requests should be filed in the [MariaDB bug ticket system](https://jira.mariadb.org/)
[licence-image]:https://img.shields.io/badge/license-GNU%20LGPL%20version%202.1-green.svg?style=flat-square
[python-image]:https://img.shields.io/badge/python-3.7-blue.svg
[python-url]:https://www.python.org/downloads/release/python-370/
@@ -0,0 +1,46 @@
mariadb-1.1.14.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
mariadb-1.1.14.dist-info/METADATA,sha256=o10KfHqiosQTjeSGQY-QL-VpfJuYLnGGqGSKNG7taYw,3372
mariadb-1.1.14.dist-info/RECORD,,
mariadb-1.1.14.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
mariadb-1.1.14.dist-info/WHEEL,sha256=UKM62ksD4jd3M6k5acFAaw8CHnQfpjto_KljRXbypP0,104
mariadb-1.1.14.dist-info/licenses/LICENSE,sha256=IMF9i4xIpgCADf0U-V1cuf9HBmqWQd3qtI3FSuyW4zE,26526
mariadb-1.1.14.dist-info/top_level.txt,sha256=lLz5H7vCj1A7w7jg0yMFXrwc-PADp2SjVl1GiJrDiWE,8
mariadb/__init__.py,sha256=K4u7Dm0M69FMj0_6LZ5STOpT4JJVlqTVtTxVugDhxwQ,6011
mariadb/__pycache__/__init__.cpython-312.pyc,,
mariadb/__pycache__/connectionpool.cpython-312.pyc,,
mariadb/__pycache__/connections.cpython-312.pyc,,
mariadb/__pycache__/cursors.cpython-312.pyc,,
mariadb/__pycache__/dbapi20.cpython-312.pyc,,
mariadb/__pycache__/field.cpython-312.pyc,,
mariadb/__pycache__/release_info.cpython-312.pyc,,
mariadb/_mariadb.cpython-312-x86_64-linux-gnu.so,sha256=N4w2vVrqBmOaYg0rWPYEyj284p9hlkuzoe_NTYHG1-w,345768
mariadb/connectionpool.py,sha256=SH1OObb3vKDk9vgjWE_wGA7sv1LEcqrlF8lsywQ9zjE,11566
mariadb/connections.py,sha256=WM29p_vqIGqxYz6F8a8A1C4Nf_DsH6UkQSqkcyIliVw,23546
mariadb/constants/CAPABILITY.py,sha256=clOg_Ce5fAP8lgPyQ-xtJ0EI-FGbJeW7rp9IV6WYfRA,831
mariadb/constants/CLIENT.py,sha256=9kCIi59s6kh7mEXWUSB8UzZmr-mfgnu4FoQLz6pezKY,828
mariadb/constants/CURSOR.py,sha256=V6PcHC8Cmt6kLuejVybuMKYY_IvPdkcLArUNWGRfgSI,209
mariadb/constants/ERR.py,sha256=q2K17DRfulpSMHaPRwl2KHiOB45CO-YJZO7ftiLmMkk,41779
mariadb/constants/EXT_FIELD_TYPE.py,sha256=69XJ1IpDBjAS98Zgzl1X9vde6EcD7mKpGl6Of9Y9Fls,355
mariadb/constants/FIELD_FLAG.py,sha256=KUlMVnQHnPJw7CsQxxL3ncs76mC5DJGfTUAltilJDOI,609
mariadb/constants/FIELD_TYPE.py,sha256=u5tYaiIde-7SVlGbtbaGV6gEg70FqEJc1ny44xgr4AI,630
mariadb/constants/INDICATOR.py,sha256=sTJ6mqwy_9NYf2tegHeZ-zLRuQ8aQgQrIe77VewISTE,354
mariadb/constants/INFO.py,sha256=u4IpUmSltwu4FESI-6bMpJnMl1xhHU6w72lSAMnp2Rs,743
mariadb/constants/STATUS.py,sha256=XDbqAslsz43g3tjOkuuY4oaQtqmATdxw2EcRLN8In5Y,426
mariadb/constants/TPC_STATE.py,sha256=ferMGNtes-xdYqayroN6l226I5ux_hZ_uxrZuErZV4s,29
mariadb/constants/__init__.py,sha256=BljXLv1ZQyIyHHTrw137vODnJdQZTkrk7QIyEUlpaA4,114
mariadb/constants/__pycache__/CAPABILITY.cpython-312.pyc,,
mariadb/constants/__pycache__/CLIENT.cpython-312.pyc,,
mariadb/constants/__pycache__/CURSOR.cpython-312.pyc,,
mariadb/constants/__pycache__/ERR.cpython-312.pyc,,
mariadb/constants/__pycache__/EXT_FIELD_TYPE.cpython-312.pyc,,
mariadb/constants/__pycache__/FIELD_FLAG.cpython-312.pyc,,
mariadb/constants/__pycache__/FIELD_TYPE.cpython-312.pyc,,
mariadb/constants/__pycache__/INDICATOR.cpython-312.pyc,,
mariadb/constants/__pycache__/INFO.cpython-312.pyc,,
mariadb/constants/__pycache__/STATUS.cpython-312.pyc,,
mariadb/constants/__pycache__/TPC_STATE.cpython-312.pyc,,
mariadb/constants/__pycache__/__init__.cpython-312.pyc,,
mariadb/cursors.py,sha256=ucKYI41d9htkRhmkOtBr8bo3tWhaotoKlsfNUieP8p4,21624
mariadb/dbapi20.py,sha256=5jmqnHYaGC1_CnKlnBfimQaMLKPAA2k7qw-uNFLtXzU,3311
mariadb/field.py,sha256=LtmB5Hq8sPveY17bpSsBney57qW2Lxq13uOYx-eMErM,2363
mariadb/release_info.py,sha256=v5u0LkjznAoDyHJkYcBKnzahtEBgPAhlsEjXjIEshLU,82
@@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: setuptools (84.0.0)
Root-Is-Purelib: false
Tag: cp312-cp312-linux_x86_64
@@ -0,0 +1,504 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random
Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
@@ -0,0 +1 @@
mariadb
@@ -0,0 +1,103 @@
'''
MariaDB Connector/Python module enables python programs to access MariaDB and
MySQL databases, using an API which is compliant with the Python DB API 2.0
(PEP-249).
'''
import mariadb
from ._mariadb import (
DataError,
DatabaseError,
Error,
IntegrityError,
InterfaceError,
InternalError,
NotSupportedError,
OperationalError,
PoolError,
ProgrammingError,
Warning,
mariadbapi_version,
_have_asan,
)
from .field import fieldinfo
from mariadb.dbapi20 import * # noqa: F401,F403
from mariadb.connectionpool import * # noqa: F401,F403
from mariadb.cursors import Cursor
from mariadb.release_info import __version__ as __version__
from mariadb.release_info import __version_info__ as __version_info__
from mariadb.release_info import __author__ as __author__
from mariadb.connections import Connection
# disable for now, until tests are in place
# from mariadb.pooling import *
_POOLS = _CONNECTION_POOLS = {}
__all__ = ["DataError", "DatabaseError", "Error", "IntegrityError",
"InterfaceError", "InternalError", "NotSupportedError",
"OperationalError", "PoolError", "ProgrammingError",
"Warning", "Connection", "__version__", "__version_info__",
"__author__", "Cursor", "fieldinfo", "_have_asan"]
def connect(*args, connectionclass=mariadb.connections.Connection, **kwargs):
"""
Creates a MariaDB Connection object.
By default, the standard connectionclass mariadb.connections.Connection
will be created.
Parameter connectionclass specifies a subclass of
mariadb.Connection object. If not specified, default will be used.
This optional parameter was added in version 1.1.0.
Connection parameters are provided as a set of keyword arguments:
- **`host`** - The host name or IP address of the database server. If MariaDB Connector/Python was built with MariaDB Connector/C 3.3, it is also possible to provide a comma separated list of hosts for simple fail over in case of one or more hosts are not available.
- **`user`, `username`** - The username used to authenticate with the database server
- **`password`, `passwd`** - The password of the given user
- **`database`, `db`** - Database (schema) name to use when connecting with the database server
- **`unix_socket`** - The location of the unix socket file to use instead of using an IP port to connect. If socket authentication is enabled, this can also be used in place of a password.
- **`port`** - Port number of the database server. If not specified, the default value of 3306 will be used.
- **`connect_timeout`** - Connect timeout in seconds
- **`read_timeout`** - Read timeout in seconds
- **`write_timeout`** - Write timeout in seconds
- **`local_infile`** - Enables or disables the use of LOAD DATA LOCAL INFILE statements.
- **`compress`** (default: `False`) - Uses the compressed protocol for client server communication. If the server doesn't support compressed protocol, the default protocol will be used.
- **`init_command`** - Command(s) which will be executed when connecting and reconnecting to the database server
- **`default_file`** - Read options from the specified option file. If the file is an empty string, default configuration file(s) will be used
- **`default_group`** - Read options from the specified group
- **`plugin_dir`** - Directory which contains MariaDB client plugins.
- **`reconnect`** - Enables or disables automatic reconnect. Available since version 1.1.4
- **`ssl_key`** - Defines a path to a private key file to use for TLS. This option requires that you use the absolute path, not a relative path. The specified key must be in PEM format
- **`ssl_cert`** - Defines a path to the X509 certificate file to use for TLS. This option requires that you use the absolute path, not a relative path. The X609 certificate must be in PEM format.
- **`ssl_ca`** - Defines a path to a PEM file that should contain one or more X509 certificates for trusted Certificate Authorities (CAs) to use for TLS. This option requires that you use the absolute path, not a relative path.
- **`ssl_capath`** - Defines a path to a directory that contains one or more PEM files that contains one X509 certificate for a trusted Certificate Authority (CA)
- **`ssl_cipher`** - Defines a list of permitted cipher suites to use for TLS
- **`ssl_crlpath`** - Defines a path to a PEM file that should contain one or more revoked X509 certificates to use for TLS. This option requires that you use the absolute path, not a relative path.
- **`ssl_verify_cert`** - Enables server certificate verification.
- **`ssl`** - The connection must use TLS security, or it will fail.
- **`tls_version`** - A comma-separated list (without whitespaces) of TLS versions. Valid versions are TLSv1.0, TLSv1.1,TLSv1.2 and TLSv1.3. Added in version 1.1.7.
- **`autocommit`** (default: `False`) - Specifies the autocommit settings. True will enable autocommit, False will disable it (default).
- **`converter`** - Specifies a conversion dictionary, where keys are FIELD_TYPE values and values are conversion functions
"""
if kwargs:
if "pool_name" in kwargs:
if not kwargs["pool_name"] in mariadb._CONNECTION_POOLS:
pool = mariadb.ConnectionPool(**kwargs)
else:
pool = mariadb._CONNECTION_POOLS[kwargs["pool_name"]]
c = pool.get_connection()
return c
connection = connectionclass(*args, **kwargs)
if not isinstance(connection, mariadb.connections.Connection):
raise mariadb.ProgrammingError("%s is not an instance of "
"mariadb.Connection" % connection)
return connection
client_version_info = tuple(int(x, 10) for x in mariadbapi_version.split('.'))
client_version = client_version_info[0] * 10000 +\
client_version_info[1] * 1000 + client_version_info[2]
@@ -0,0 +1,301 @@
#
# Copyright (C) 2020-2021 Georg Richter and MariaDB Corporation AB
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
# You should have received a copy of the GNU Library General Public
# License along with this library; if not see <http://www.gnu.org/licenses>
# or write to the Free Software Foundation, Inc.,
# 51 Franklin St., Fifth Floor, Boston, MA 02110, USA
#
import mariadb
import _thread
import time
from mariadb.constants import STATUS
MAX_POOL_SIZE = 64
class ConnectionPool(object):
"""
Class defining a pool of database connections
MariaDB Connector/Python supports simple connection pooling.
A connection pool holds a number of open connections and handles thread safety when providing connections to threads.
The size of a connection pool is configurable at creation time, but cannot be changed afterward. The maximum size of a connection pool is limited to 64 connections.
Keyword Arguments:
- **`pool_name`** (``str``) - Name of connection pool
- **`pool_size`** (``int``) - Size of pool. The Maximum allowed number is 64. Default to 5
- **`pool_reset_connection`** (``bool``) - Will reset the connection before returning it to the pool. Default to True.
- **`pool_validation_interval`** (``int``) - Specifies the validation interval in milliseconds after which the status of a connection requested from the pool is checked. A value of 0 means that the status will always be checked. Default to 500 (Added in version 1.1.6)
- **\*\*kwargs** - Optional additional connection arguments, as described in mariadb.connect() method.
"""
def __init__(self, *args, **kwargs):
"""
Creates a connection pool class
:param str pool_name:
Name of connection pool
:param int pool_size:
Size of pool. If not specified, the default value of 5 will be used.
The Maximum allowed number is 64.
:param bool pool_reset_connection:
Will reset the connection before returning it to the pool.
The Default value is True.
:param **kwargs kwargs:
Optional additional connection arguments, as described in
mariadb.connect() method.
"""
self._connections_free = []
self._connections_used = []
self._pool_args = {}
self._conn_args = {}
self._lock_pool = _thread.RLock()
self.__closed = 0
key_words = ["pool_name", "pool_size", "pool_reset_connection",
"pool_validation_interval"]
# check if pool_name was provided
if kwargs and "pool_name" in kwargs:
# check if pool_name already exists
if kwargs["pool_name"] in mariadb._CONNECTION_POOLS:
raise mariadb.ProgrammingError("Pool '%s' already exists"
% kwargs["pool_name"])
else:
raise mariadb.ProgrammingError("No pool name specified")
# save pool keyword arguments
self._pool_args["name"] = kwargs.get("pool_name")
self._pool_args["size"] = int(kwargs.get("pool_size", 5))
self._pool_args["reset_connection"] = \
bool(kwargs.get("pool_reset_connection", True))
self._pool_args["validation_interval"] = \
int(kwargs.get("pool_validation_interval", 500))
# validate pool size (must be in range between 1 and MAX_POOL_SIZE)
if not (0 < self._pool_args["size"] <= MAX_POOL_SIZE):
raise mariadb.ProgrammingError("Pool size must be in range of "
"1 and %s" % MAX_POOL_SIZE)
# store pool and connection arguments
self._conn_args = kwargs.copy()
for key in key_words:
if key in self._conn_args:
del self._conn_args[key]
if len(self._conn_args) > 0:
with self._lock_pool:
# fill connection pool
for i in range(0, self._pool_args["size"]):
try:
connection = mariadb.Connection(**self._conn_args)
except mariadb.Error:
# if an error occurred, close all connections
# and raise exception
for j in reversed(range(0, len(self._connections_free))):
try:
self._connections_free[j].close()
except mariadb.Error:
# connect failed, so we are not
# interested in errors
# from close() method
pass
del self._connections_free[j]
raise
self.add_connection(connection)
# store connection pool in _CONNECTION_POOLS
mariadb._CONNECTION_POOLS[self._pool_args["name"]] = self
def _replace_connection(self, connection):
"""
Removes the given connection and adds a new connection.
"""
if connection:
if connection in self._connections_free:
x = self._connections_free.index(connection)
del self._connections_free[x]
elif connection in self._connections_used:
x = self._connections_used.index(connection)
del self._connections_used[x]
connection._Connection__pool = None
connection.close()
return self.add_connection()
def __repr__(self):
if (self.__closed):
return "<mariadb.connectionPool.ConnectionPool object (closed) "\
"at %s>" % (hex(id(self)),)
else:
return "<mariadb.connectionPool.ConnectionPool object (name=%s) "\
"at %s>" % (self.pool_name, hex(id(self)))
def add_connection(self, connection=None):
"""
Adds a connection object to the connection pool.
In case that the pool doesn’t have a free slot or is not configured,
a PoolError exception will be raised.
"""
if not self._conn_args:
raise mariadb.PoolError("Couldn't get configuration for pool %s" %
self._pool_args["name"])
if (connection is not None and
not isinstance(connection, mariadb.connections.Connection)):
raise mariadb.ProgrammingError("Passed parameter is not a "
"connection object")
if connection is None and len(self._conn_args) == 0:
raise mariadb.PoolError("Can't get configuration for pool %s" %
self._pool_args["name"])
total = len(self._connections_free + self._connections_used)
if total >= self._pool_args["size"]:
raise mariadb.PoolError("Can't add connection to pool %s: "
"No free slot available (%s)." %
(self._pool_args["name"],
total))
with self._lock_pool:
if connection is None:
connection = mariadb.Connection(**self._conn_args)
connection._Connection__pool = self
connection.__last_used = time.perf_counter_ns()
self._connections_free.append(connection)
return connection
def get_connection(self):
"""
Returns a connection from the connection pool or raises a PoolError
exception if a connection is not available.
"""
conn = None
with self._lock_pool:
for i in range(0, len(self._connections_free)):
conn = self._connections_free[i]
dt = (time.perf_counter_ns() - conn.__last_used) / 1000000
if dt > self._pool_args["validation_interval"]:
try:
conn.ping()
except mariadb.Error:
conn = self._replace_connection(conn)
if not conn:
continue
conn._used += 1
self._connections_used.append(conn)
idx = self._connections_free.index(conn)
del self._connections_free[idx]
return conn
raise mariadb.PoolError("No connection available")
def _close_connection(self, connection):
"""
Returns connection to the pool. Internally used
by connection object.
"""
with self._lock_pool:
try:
if self._pool_args["reset_connection"]:
connection.reset()
elif connection.server_status & STATUS.IN_TRANS:
connection.rollback()
except mariadb.Error:
self._replace_connection(connection)
if connection:
if connection in self._connections_used:
x = self._connections_used.index(connection)
del self._connections_used[x]
connection.__last_used = time.perf_counter_ns()
self._connections_free.append(connection)
def set_config(self, **kwargs):
"""
Sets the connection configuration for the connection pool.
For valid connection arguments, check the mariadb.connect() method.
Note: This method doesn't create connections in the pool.
To fill the pool, one has to use add_connection() ḿethod.
"""
self._conn_args = kwargs
def close(self):
"""Closes connection pool and all connections."""
try:
for c in (self._connections_free + self._connections_used):
c._Connection__pool = None
c.close()
finally:
self._connections_free = None
self._connections_used = None
del mariadb._CONNECTION_POOLS[self._pool_args["name"]]
@property
def pool_name(self):
"""Returns the name of the connection pool."""
return self._pool_args["name"]
@property
def pool_size(self):
"""Returns the size of the connection pool."""
return self._pool_args["size"]
@property
def max_size(self):
"Returns the maximum size for connection pools."""
return MAX_POOL_SIZE
@property
def connection_count(self):
"Returns the number of connections in connection pool."""
try:
return len(self._connections_free + self._connections_used)
except Exception:
return 0
@property
def pool_reset_connection(self):
"""
If set to true, the connection will be reset on both client and server
side after .close() method was called
"""
return self._pool_args["reset_connection"]
@pool_reset_connection.setter
def pool_reset_connection(self, reset):
self._pool_args["reset_connection"] = reset
@@ -0,0 +1,692 @@
#
# Copyright (C) 2020-2021 Georg Richter and MariaDB Corporation AB
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
# You should have received a copy of the GNU Library General Public
# License along with this library; if not see <http://www.gnu.org/licenses>
# or write to the Free Software Foundation, Inc.,
# 51 Franklin St., Fifth Floor, Boston, MA 02110, USA
#
import mariadb
import socket
import mariadb.cursors
from mariadb.constants import STATUS, TPC_STATE, INFO
from packaging import version
_DEFAULT_CHARSET = "utf8mb4"
_DEFAULT_COLLATION = "utf8mb4_general_ci"
_MAX_TPC_XID_SIZE = 64
class Connection(mariadb._mariadb.connection):
"""
MariaDB Connector/Python Connection Object
Handles the connection to a MariaDB or MySQL database server.
It encapsulates a database session.
Connections are created using the method mariadb.connect()
"""
def _check_closed(self):
if self._closed:
raise mariadb.ProgrammingError("Invalid connection or "
"not connected")
def __init__(self, *args, **kwargs):
"""
Establishes a connection to a database server and returns a connection
object.
"""
self._socket = None
self._used = 0
self._last_executed_statement = None
self._socket = None
self.__pool = None
self.__last_used = 0
self.tpc_state = TPC_STATE.NONE
self._xid = None
autocommit = kwargs.pop("autocommit", False)
reconnect = kwargs.pop("reconnect", False)
self._converter = kwargs.pop("converter", None)
# if host contains a connection string or multiple hosts,
# we need to check if it's supported by Connector/C
if "host" in kwargs:
host = kwargs.get("host")
if version.Version(mariadb.mariadbapi_version) <\
version.Version('3.3.0') and ',' in host:
raise mariadb.ProgrammingError("Host failover list requires "
"MariaDB Connector/C 3.3.0 "
"or newer")
# compatibility feature: if SSL is provided as a dictionary,
# we will map it's content
if "ssl" in kwargs and not isinstance(kwargs["ssl"], bool):
ssl = kwargs.pop("ssl", None)
for key in ["ca", "cert", "capath", "key", "cipher"]:
if key in ssl:
kwargs["ssl_%s" % key] = ssl[key]
kwargs["ssl"] = True
super().__init__(*args, **kwargs)
self.autocommit = autocommit
self.auto_reconnect = reconnect
def cursor(self, cursorclass=mariadb.cursors.Cursor, **kwargs):
"""
Returns a new cursor object for the current connection.
If no cursorclass was specified, a cursor with default mariadb.Cursor class will be created.
Optional keyword parameters:
- **buffered** (default: ``True``) - If disabled, the result will be unbuffered, which means before executing another statement with the same connection, the entire result set must be fetched. Please note that the default was False for MariaDB Connector/Python versions < 1.1.0.
- **dictionary** (default: ``False``) - Return fetch values as dictionary when enabled.
- **named_tuple** (default: ``False``) - Return fetch values as named tuple. This feature exists for compatibility reasons and should be avoided due to possible inconsistency.
- **cursor_type** (default: ``CURSOR.NONE``) - If cursor_type is set to CURSOR.READ_ONLY, a cursor is opened for the statement invoked with cursors execute() method.
- **prepared** (default: ``False``) - When enabled, the cursor will remain in prepared state after the first execute() method was called. Further calls to execute() method will ignore the SQL statement.
- **binary** (default: ``False``) - Always execute statement in MariaDB client/server binary protocol.
In versions prior to 1.1.0 results were unbuffered by default, which means before executing another statement with the same connection, the entire result set must be fetched.
fetch* methods of the cursor class by default return result set values as a tuple, unless dictionary or named_tuple was specified. The latter one exists for compatibility reasons and should be avoided due to possible inconsistency in case two or more fields in a result set have the same name.
If cursor_type is set to CURSOR.READ_ONLY, a cursor is opened for the statement invoked with cursors execute() method.
"""
self._check_closed()
cursor = cursorclass(self, **kwargs)
if not isinstance(cursor, mariadb._mariadb.cursor):
raise mariadb.ProgrammingError("%s is not an instance of "
"mariadb.cursor" % cursor)
return cursor
def close(self):
self._check_closed()
if self._Connection__pool:
self._Connection__pool._close_connection(self)
else:
super().close()
def __enter__(self):
self._check_closed()
"Returns a copy of the connection."
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self._check_closed()
"Closes connection."
self.close()
def commit(self):
"""
Commit any pending transaction to the database.
"""
self._check_closed()
if self.tpc_state > TPC_STATE.NONE:
raise mariadb.ProgrammingError("commit() is not allowed if "
"a TPC transaction is active")
self._execute_command("COMMIT")
self._read_response()
def rollback(self):
"""
Causes the database to roll back to the start of any pending
transaction
Closing a connection without committing the changes first will
cause an implicit rollback to be performed.
Note that rollback() will not work as expected if autocommit mode
was set to True or the storage engine does not support transactions."
"""
self._check_closed()
if self.tpc_state > TPC_STATE.NONE:
raise mariadb.ProgrammingError("rollback() is not allowed if a "
"TPC transaction is active")
self._execute_command("ROLLBACK")
self._read_response()
def kill(self, id: int):
"""
This function is used to ask the server to kill a database connection
specified by the processid parameter.
The connection id can be retrieved by `SHOW PROCESSLIST` SQL command.
"""
self._check_closed()
if not isinstance(id, int):
raise mariadb.ProgrammingError("id must be of type int.")
stmt = "KILL %s" % id
self._execute_command(stmt)
self._read_response()
def begin(self):
"""
Start a new transaction which can be committed by .commit() method,
or canceled by .rollback() method.
"""
self._check_closed()
self._execute_command("BEGIN")
self._read_response()
def select_db(self, new_db: str):
"""
Gets the default database for the current connection.
The default database can also be obtained or changed by database
attribute.
"""
self._check_closed()
self.database = new_db
def get_server_version(self):
"""
Returns a tuple representing the version of the connected server in
the following format: (MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
"""
return self.server_version_info
def show_warnings(self):
"""
Shows error, warning and note messages from last executed command.
"""
self._check_closed()
if (not self.warnings):
return None
cursor = self.cursor()
cursor.execute("SHOW WARNINGS")
ret = cursor.fetchall()
del cursor
return ret
class xid(tuple):
"""
xid(format_id: int, global_transaction_id: str, branch_qualifier: str)
Creates a transaction ID object suitable for passing to the .tpc_*()
methods of this connection.
Parameters:
- format_id: Format id. Default to value `0`.
- global_transaction_id: Global transaction qualifier, which must be
unique. The maximum length of the global transaction id is
limited to 64 characters.
- branch_qualifier: Branch qualifier which represents a local
transaction identifier. The maximum length of the branch qualifier
is limited to 64 characters.
"""
def __new__(self, format_id, transaction_id, branch_qualifier):
if not isinstance(format_id, int):
raise mariadb.ProgrammingError("argument 1 must be int, "
"not %s",
type(format_id).__name__)
if not isinstance(transaction_id, str):
raise mariadb.ProgrammingError("argument 2 must be str, "
"not %s",
type(transaction_id).__mane__)
if not isinstance(branch_qualifier, str):
raise mariadb.ProgrammingError("argument 3 must be str, "
"not %s",
type(transaction_id).__name__)
if len(transaction_id) > _MAX_TPC_XID_SIZE:
raise mariadb.ProgrammingError("Maximum length of "
"transaction_id exceeded.")
if len(branch_qualifier) > _MAX_TPC_XID_SIZE:
raise mariadb.ProgrammingError("Maximum length of "
"branch_qualifier exceeded.")
if format_id == 0:
format_id = 1
return super().__new__(self, (format_id,
transaction_id,
branch_qualifier))
def tpc_begin(self, xid):
"""
Parameter:
xid: xid object which was created by .xid() method of connection
class
Begins a TPC transaction with the given transaction ID xid.
This method should be called outside a transaction
(i.e., nothing may have been executed since the last .commit()
or .rollback()).
Furthermore, it is an error to call .commit() or .rollback() within
the TPC transaction. A ProgrammingError is raised if the application
calls .commit() or .rollback() during an active TPC transaction.
"""
self._check_closed()
if type(xid).__name__ != "xid":
raise mariadb.ProgrammingError("argument 1 must be xid "
"not %s", type(xid).__name__)
stmt = "XA BEGIN '%s','%s',%s" % (xid[1], xid[2], xid[0])
try:
self._execute_command(stmt)
self._read_response()
except mariadb.Error:
raise
self.tpc_state = TPC_STATE.XID
self._xid = xid
def tpc_commit(self, xid=None):
"""
Optional parameter:
- xid
: xid object which was created by .xid() method of connection class.
When called with no arguments, .tpc_commit() commits a TPC transaction
previously prepared with .tpc_prepare().
If .tpc_commit() is called prior to .tpc_prepare(), a single phase
commit is performed. A transaction manager may choose to do this if
only a single resource is participating in the global transaction.
When called with a transaction ID xid, the database commits the given
transaction. If an invalid transaction ID is provided,
a ProgrammingError will be raised.
This form should be called outside a transaction, and
is intended for use in recovery.
"""
self._check_closed()
if not xid:
xid = self._xid
if self.tpc_state == TPC_STATE.NONE:
raise mariadb.ProgrammingError("Transaction not started.")
if xid is None and self.tpc_state != TPC_STATE.PREPARE:
raise mariadb.ProgrammingError("Transaction is not prepared.")
if xid and type(xid).__name__ != "xid":
raise mariadb.ProgrammingError("argument 1 must be xid "
"not %s" % type(xid).__name__)
if self.tpc_state < TPC_STATE.PREPARE:
stmt = "XA END '%s','%s',%s" % (xid[1], xid[2], xid[0])
self._execute_command(stmt)
try:
self._read_response()
except mariadb.Error:
self._xid = None
self.tpc_state = TPC_STATE.NONE
raise
stmt = "XA COMMIT '%s','%s',%s" % (xid[1], xid[2], xid[0])
if self.tpc_state < TPC_STATE.PREPARE:
stmt = stmt + " ONE PHASE"
try:
self._execute_command(stmt)
self._read_response()
except mariadb.Error:
self._xid = None
self.tpc_state = TPC_STATE.NONE
raise
# cleanup
self._xid = None
self.tpc_state = TPC_STATE.NONE
def tpc_prepare(self):
"""
Performs the first phase of a transaction started with .tpc_begin().
A ProgrammingError will be raised if this method was called outside
a TPC transaction.
After calling .tpc_prepare(), no statements can be executed until
.tpc_commit() or .tpc_rollback() have been called.
"""
self._check_closed()
if self.tpc_state == TPC_STATE.NONE:
raise mariadb.ProgrammingError("Transaction not started.")
if self.tpc_state == TPC_STATE.PREPARE:
raise mariadb.ProgrammingError("Transaction is already in "
"prepared state.")
xid = self._xid
stmt = "XA END '%s','%s',%s" % (xid[1], xid[2], xid[0])
try:
self._execute_command(stmt)
self._read_response()
except mariadb.Error:
self._xid = None
self.tpc_state = TPC_STATE.NONE
raise
stmt = "XA PREPARE '%s','%s',%s" % (xid[1], xid[2], xid[0])
try:
self._execute_command(stmt)
self._read_response()
except mariadb.Error:
self._xid = None
self.tpc_state = TPC_STATE.NONE
raise
self.tpc_state = TPC_STATE.PREPARE
def tpc_rollback(self, xid=None):
"""
Parameter:
xid: xid object which was created by .xid() method of connection
class
Performs the first phase of a transaction started with .tpc_begin().
A ProgrammingError will be raised if this method outside a TPC
transaction.
After calling .tpc_prepare(), no statements can be executed until
.tpc_commit() or .tpc_rollback() have been called.
"""
self._check_closed()
if self.tpc_state == TPC_STATE.NONE:
raise mariadb.ProgrammingError("Transaction not started.")
if xid and type(xid).__name__ != "xid":
raise mariadb.ProgrammingError("argument 1 must be xid "
"not %s" % type(xid).__name__)
if not xid:
xid = self._xid
if self.tpc_state < TPC_STATE.PREPARE:
stmt = "XA END '%s','%s',%s" % (xid[1], xid[2], xid[0])
self._execute_command(stmt)
try:
self._read_response()
except mariadb.Error:
self._xid = None
self.tpc_state = TPC_STATE.NONE
raise
stmt = "XA ROLLBACK '%s','%s',%s" % (xid[1], xid[2], xid[0])
try:
self._execute_command(stmt)
self._read_response()
except mariadb.Error:
self._xid = None
self.tpc_state = TPC_STATE.NONE
raise
self.tpc_state = TPC_STATE.PREPARE
def tpc_recover(self):
"""
Returns a list of pending transaction IDs suitable for use with
tpc_commit(xid) or .tpc_rollback(xid).
"""
self._check_closed()
cursor = self.cursor()
cursor.execute("XA RECOVER")
result = cursor.fetchall()
del cursor
return result
@property
def tls_peer_cert_info(self):
"""Get peer certificate information."""
if version.Version(mariadb.mariadbapi_version) <\
version.Version('3.4.2'):
return None
self._check_closed()
if self._tls:
return self._mariadb_get_info(INFO.TLS_PEER_CERT_INFO)
return None
@property
def database(self):
"""Get the current database of the connection."""
self._check_closed()
return self._mariadb_get_info(INFO.SCHEMA)
@database.setter
def database(self, schema):
"""Set default database."""
self._check_closed()
try:
self._execute_command("USE %s" % str(schema))
self._read_response()
except mariadb.Error:
raise
@property
def user(self):
"""
Returns the username for the current connection or empty
string if it can't be determined, e.g., when using socket
authentication.
"""
self._check_closed()
return self._mariadb_get_info(INFO.USER)
@property
def character_set(self):
"""
Client character set.
For MariaDB Connector/Python, it is always utf8mb4.
"""
return _DEFAULT_CHARSET
@property
def client_capabilities(self):
"""Client capability flags."""
self._check_closed()
return self._mariadb_get_info(INFO.CLIENT_CAPABILITIES)
@property
def server_capabilities(self):
"""Server capability flags."""
self._check_closed()
return self._mariadb_get_info(INFO.SERVER_CAPABILITIES)
@property
def extended_server_capabilities(self):
"""
Extended server capability flags (only for MariaDB
database servers).
"""
self._check_closed()
return self._mariadb_get_info(INFO.EXTENDED_SERVER_CAPABILITIES)
@property
def server_port(self):
"""
Database server TCP/IP port. This value will be 0 in case of an unix
socket connection.
"""
self._check_closed()
return self._mariadb_get_info(INFO.PORT)
@property
def unix_socket(self):
"""Unix socket name."""
self._check_closed()
return self._mariadb_get_info(INFO.UNIX_SOCKET)
@property
def server_name(self):
"""Name or IP address of database server."""
self._check_closed()
return self._mariadb_get_info(INFO.HOST)
@property
def collation(self):
"""Client character set collation"""
return _DEFAULT_COLLATION
@property
def server_info(self):
"""Server version in alphanumerical format (str)"""
self._check_closed()
return self._mariadb_get_info(INFO.SERVER_VERSION)
@property
def tls_cipher(self):
"""TLS cipher suite if a secure connection is used."""
self._check_closed()
if self._tls:
return self._mariadb_get_info(INFO.SSL_CIPHER)
return None
@property
def tls_version(self):
"""TLS protocol version if a secure connection is used."""
self._check_closed()
if self._tls:
return self._mariadb_get_info(INFO.TLS_VERSION)
return None
@property
def _tls_verify_status(self):
"""Returns the result of the peer certificate verification."""
if version.Version(mariadb.mariadbapi_version) <\
version.Version('3.4.2'):
return None
self._check_closed()
if self._tls:
return self._mariadb_get_info(INFO.TLS_VERIFY_STATUS)
return None
@property
def server_status(self):
"""
Return server status flags
"""
self._check_closed()
return self._mariadb_get_info(INFO.SERVER_STATUS)
@property
def server_version(self):
"""
Server version in numerical format.
The form of the version number is
VERSION_MAJOR * 10000 + VERSION_MINOR * 100 + VERSION_PATCH
"""
self._check_closed()
return self._mariadb_get_info(INFO.SERVER_VERSION_ID)
@property
def server_version_info(self):
"""
Returns numeric version of connected database server in tuple format.
"""
self._check_closed()
version = self.server_version
return (int(version / 10000),
int((version % 10000) / 100),
version % 100)
@property
def autocommit(self):
"""
Toggles autocommit mode on or off for the current database connection.
Autocommit mode only affects operations on transactional table types.
Be aware that rollback() will not work if autocommit mode was switched
on.
By default, autocommit mode is set to False.
"""
self._check_closed()
return bool(self.server_status & STATUS.AUTOCOMMIT)
@autocommit.setter
def autocommit(self, mode):
self._check_closed()
if bool(mode) == self.autocommit:
return
try:
self._execute_command("SET AUTOCOMMIT=%s" % int(mode))
self._read_response()
except mariadb.Error:
raise
@property
def socket(self):
"""Returns the socket used for database connection"""
fno = self._get_socket()
if not self._socket:
self._socket = socket.socket(fileno=fno)
# in case of a possible reconnect, file descriptor has changed
elif fno != self._socket.fileno():
self._socket = socket.socket(fileno=fno)
return self._socket
@property
def open(self):
"""
Returns true if the connection is alive.
A ping command will be sent to the server for this purpose,
which means this function might fail if there are still
non-processed pending result sets.
"""
self._check_closed()
try:
self.ping()
except mariadb.Error:
return False
return True
# Aliases
character_set_name = character_set
@property
def thread_id(self):
"""
Alias for connection_id
"""
self._check_closed()
return self.connection_id
@@ -0,0 +1,38 @@
'''
MariaDB capability flags.
These flags are used to check the capabilities both of a MariaDB server
or the client applicaion.
Capability flags are defined in module *mariadb.constants.CAPABILIY*
'''
MYSQL = 1 # MariaDB
LONG_PASSWORD = 1 # MySQL
FOUND_ROWS = 2
LONG_FLAG = 4
CONNECT_WITH_DB = 8
NO_SCHEMA = 16
COMPRESS = 32
LOCAL_FILES = 128
IGNORE_SPACE = 256
INTERACTIVE = 1024
SSL = 2048
TRANSACTIONS = 8192
SECURE_CONNECTION = 32768
MULTI_STATEMENTS = 1 << 16
MULTI_RESULTS = 1 << 17
PS_MULTI_RESULTS = 1 << 18
PLUGIN_AUTH = 1 << 19
CONNECT_ATTRS = 1 << 20
CAN_HANDLE_EXPIRED_PASSWORDS = 1 < 22
SESSION_TRACKING = 1 << 23
SSL_VERIFY_SERVER_CERT = 1 << 30
REMEMBER_OPTIONS = 1 << 31
# MariaDB specific capabilities
PROGRESS = 1 << 32
BULK_OPERATIONS = 1 << 34
EXTENDED_METADATA = 1 << 35
CACHE_METDATA = 1 << 36
@@ -0,0 +1,38 @@
'''
MariaDB capability flags.
These flags are used to check the capabilities both of a MariaDB server
or the client applicaion.
Capability flags are defined in module *mariadb.constants.CLIENT*
'''
MYSQL = 1 # MariaDB
LONG_PASSWORD = 1 # MySQL
FOUND_ROWS = 2
LONG_FLAG = 4
CONNECT_WITH_DB = 8
NO_SCHEMA = 16
COMPRESS = 32
LOCAL_FILES = 128
IGNORE_SPACE = 256
INTERACTIVE = 1024
SSL = 2048
TRANSACTIONS = 8192
SECURE_CONNECTION = 32768
MULTI_STATEMENTS = 1 << 16
MULTI_RESULTS = 1 << 17
PS_MULTI_RESULTS = 1 << 18
PLUGIN_AUTH = 1 << 19
CONNECT_ATTRS = 1 << 20
CAN_HANDLE_EXPIRED_PASSWORDS = 1 < 22
SESSION_TRACKING = 1 << 23
SSL_VERIFY_SERVER_CERT = 1 << 30
REMEMBER_OPTIONS = 1 << 31
# MariaDB specific capabilities
PROGRESS = 1 << 32
BULK_OPERATIONS = 1 << 34
EXTENDED_METADATA = 1 << 35
CACHE_METDATA = 1 << 36
@@ -0,0 +1,9 @@
"""
Cursor constants are used for server side cursors.
Currently only read only cursor is supported.
Cursor constants are defined in module *mariadb.constants.CURSOR*.
"""
NONE = 0
READ_ONLY = 1
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
"""
MariaDB EXT_FIELD_TYPE Constants
These constants represent the extended field types supported by MariaDB.
Extended field types are defined in module *mariadb.constants.EXT_FIELD_TYPE*
"""
NONE =0
JSON = 1
UUID = 2
INET4 = 3
INET6 = 4
POINT = 5
MULTIPOINT = 6
LINESTRING = 7
MULTILINESTRING = 8
POLYGON = 9
MULTIPOLYGON = 10
GEOMETRYCOLLECTION = 11
@@ -0,0 +1,29 @@
"""MariaDB FIELD_FLAG Constants
These constants represent the various field flags. As an addition
to the DBAPI 2.0 standard (PEP-249) these flags are returned as
eighth element of the cursor description attribute.
Field flags are defined in module *mariadb.constants.FIELD_FLAG*
"""
# Source: mariadb_com.h (MariaDB Connector(C)
NOT_NULL = 1
PRIMARY_KEY = 2
UNIQUE_KEY = 4
MULTIPLE_KEY = 8
BLOB = 16
UNSIGNED = 32
ZEROFILL = 64
BINARY = 128
ENUM = 256
AUTO_INCREMENT = 512
TIMESTAMP = 1024
SET = 2048
NO_DEFAULT = 4096
ON_UPDATE_NOW = 8192
NUMERIC = 32768
PART_OF_KEY = 16384
GROUP = 32768
UNIQUE = 65536
@@ -0,0 +1,40 @@
"""
MariaDB FIELD_TYPE Constants
These constants represent the field types supported by MariaDB.
The field type is returned as second element of cursor description attribute.
Field types are defined in module *mariadb.constants.FIELD_TYPE*
"""
DECIMAL = 0
TINY = 1
SHORT = 2
LONG = 3
FLOAT = 4
DOUBLE = 5
NULL = 6
TIMESTAMP = 7
LONGLONG = 8
INT24 = 9
DATE = 10
TIME = 11
DATETIME = 12
YEAR = 13
NEWDATE = 14
VARCHAR = 15
BIT = 16
TIMESTAMP2 = 17
DATETIME2 = 18
TIME2 = 19
JSON = 245
NEWDECIMAL = 246
ENUM = 247
SET = 248
TINY_BLOB = 249
MEDIUM_BLOB = 250
LONG_BLOB = 251
BLOB = 252
VAR_STRING = 253
STRING = 254
GEOMETRY = 255
@@ -0,0 +1,19 @@
'''
MariaDB indicator variables
Indicator values are used in executemany() method of cursor class to
indicate special values.
'''
class MrdbIndicator():
indicator = 0
def __init__(self, indicator):
self.indicator = indicator
NULL = MrdbIndicator(1)
DEFAULT = MrdbIndicator(2)
IGNORE = MrdbIndicator(3)
IGNORE_ROW = MrdbIndicator(4)
@@ -0,0 +1,42 @@
"""
Constants for _get_info method of MariadB connection object
"""
CHARSET_ID = 0
CHARSET_NAME = 1
CLIENT_ERRORS = 2
CLIENT_VERSION = 3
CLIENT_VERSION_ID = 4
ASYNC_TIMEOUT = 5
ASYNC_TIMEOUT_MS = 6
CHARSET_INFO = 7
ERROR = 8
ERROR_ID = 9
HOST = 10
INFO = 11
PORT = 12
PROTOCOL_VERSION_ID = 13
PVIO_TYPE = 14
SCHEMA = 15
SERVER_TYPE = 16
SERVER_VERSION = 17
SERVER_VERSION_ID = 18
SOCKET = 19
SQLSTATE = 20
SSL_CIPHER = 21
TLS_LIBRARY = 22
TLS_VERSION = 23
TLS_VERSION_ID = 24
TYPE = 25
UNIX_SOCKET = 26
USER = 27
MAX_ALLOWED_PACKET = 28
NET_BUFFER_LENGTH = 29
SERVER_STATUS = 30
SERVER_CAPABILITIES = 31
EXTENDED_SERVER_CAPABILITIES = 32
CLIENT_CAPABILITIES = 33
BYTES_READ = 34
BYTES_SENT = 35
TLS_PEER_CERT_INFO = 36
TLS_VERIFY_STATUS = 37
@@ -0,0 +1,21 @@
'''
MariaDB status flags
These flags describe the current status of the database server.
'''
IN_TRANS = 1
AUTOCOMMIT = 2
MORE_RESULTS_EXIST = 8
QUERY_NO_GOOD_INDEX_USED = 16
QUERY_NO_INDEX_USED = 32
CURSOR_EXISTS = 64
LAST_ROW_SENT = 128
DB_DROPPED = 256
NO_BACKSLASH_ESCAPES = 512
METADATA_CHANGED = 1024
QUERY_WAS_SLOW = 2048
PS_OUT_PARAMS = 4096
IN_TRANS_READONLY = 8192
SESSION_STATE_CHANGED = 16384
ANSI_QUOTES = 32768
@@ -0,0 +1,3 @@
NONE = 0
XID = 1
PREPARE = 2
@@ -0,0 +1,2 @@
__all__ = ["CLIENT", "CURSOR", "FIELD_TYPE", "FIELD_FLAG",
"INDICATOR", 'STATUS', 'ERR', 'CAPABILITY']
@@ -0,0 +1,623 @@
#
# Copyright (C) 2020-2021 Georg Richter and MariaDB Corporation AB
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Library General Public License for more details.
# You should have received a copy of the GNU Library General Public
# License along with this library; if not see <http://www.gnu.org/licenses>
# or write to the Free Software Foundation, Inc.,
# 51 Franklin St., Fifth Floor, Boston, MA 02110, USA
#
import mariadb
import datetime
from numbers import Number
from mariadb.constants import CURSOR, STATUS, CAPABILITY, INDICATOR
from typing import Sequence
import decimal
PARAMSTYLE_QMARK = 1
PARAMSTYLE_FORMAT = 2
PARAMSTYLE_PYFORMAT = 3
ROWS_ALL = -1
RESULT_TUPLE = 0
RESULT_NAMEDTUPLE = 1
RESULT_DICTIONARY = 2
# Command types
SQL_NONE = 0,
SQL_INSERT = 1
SQL_UPDATE = 2
SQL_REPLACE = 3
SQL_DELETE = 4
SQL_CALL = 5
SQL_DO = 6
SQL_SELECT = 7
SQL_OTHER = 255
ROWS_EOF = -1
class Cursor(mariadb._mariadb.cursor):
"""
MariaDB Connector/Python Cursor Object
"""
def check_closed(self):
if self._thread_id != self.connection.thread_id:
raise mariadb.ProgrammingError(f"Cursor cannot be used anymore (the connection aborted and reconnected).")
if self._closed:
raise mariadb.ProgrammingError("Cursor cannot be used anymore (it was already closed before).")
self._connection._check_closed()
def __init__(self, connection, **kwargs):
"""
initialization
"""
self._bulk = False
self._dictionary = False
self._named_tuple = False
self._connection = connection
self._resulttype = RESULT_TUPLE
self._description = None
self._transformed_statement = None
self._prepared = False
self._prev_stmt = None
self._force_binary = None
self._rowcount = 0
self.buffered = True
self._parseinfo = None
self._data = None
self._thread_id = connection.thread_id
self._closed= None
if not connection:
raise mariadb.ProgrammingError("Invalid or no connection provided")
# parse keywords
if kwargs:
rtype = kwargs.pop("named_tuple", False)
if rtype:
self._resulttype = RESULT_NAMEDTUPLE
else:
rtype = kwargs.pop("dictionary", False)
if rtype:
self._resulttype = RESULT_DICTIONARY
buffered = kwargs.pop("buffered", True)
self.buffered = buffered
self._prepared = kwargs.pop("prepared", False)
self._force_binary = kwargs.pop("binary", False)
self._cursor_type = kwargs.pop("cursor_type", 0)
# call initialization of main class
super().__init__(connection, **kwargs)
def _substitute_parameters(self):
"""
Internal use only.
When running in text protocol, this method will replace placeholders
by supplied values.
For values which aren't numbers, strings or bytes string representation
will be used.
"""
new_stmt = self.statement.encode("utf8")
replace_diff = 0
if self._paramlist:
for i in range(0, len(self._paramlist)):
extra_bytes = 0
if self._paramstyle == PARAMSTYLE_PYFORMAT:
val = self._data[self._keys[i]]
else:
val = self._data[i]
if val is None:
replace = "NULL"
else:
if isinstance(val, INDICATOR.MrdbIndicator):
if val == INDICATOR.NULL:
replace = "NULL"
if val == INDICATOR.DEFAULT:
replace = "DEFAULT"
elif isinstance(val, Number):
replace = val.__str__()
else:
if isinstance(val, (bytes, bytearray)):
replace = "'%s'" % self.connection.escape_string(
val.decode(encoding='latin1'))
else:
replace = "'%s'" % self.connection.escape_string(
val.__str__())
extra_bytes = len(replace.encode("utf-8")) -\
len(replace)
ofs = self._paramlist[i] + replace_diff
new_stmt = new_stmt[:ofs] + replace.__str__().encode("utf8") +\
new_stmt[ofs+1:]
replace_diff += len(replace) - 1 + extra_bytes
return new_stmt
def _check_decimal_parameter(self, val):
"""
Internal use only
Checks for unsupported parameters.
The following parameters are not supported by MariaDB/MySQL (but also
not part of the SQL Standard)
- float("nan" | "inf" | "-inf")
- Decimal("NaN" | "sNaN" | "Infinity" | "-Infinity")
"""
if isinstance(val, float) and repr(val) in ("nan", "inf", "-inf"):
raise mariadb.NotSupportedError(f"Float value '{repr(val)}' is not supported.")
elif isinstance(val, decimal.Decimal) and val.__str__() in ("NaN", "sNaN", "Infinity", "-Infinity"):
raise mariadb.NotSupportedError(f"Decimal value '{val.__str__()}' is not supported.")
return None
def _check_execute_params(self):
# check data format
values = ()
if self._paramstyle in (PARAMSTYLE_QMARK, PARAMSTYLE_FORMAT):
if not isinstance(self._data, (tuple, list)):
raise mariadb.ProgrammingError("Data argument must be "
"Tuple or List")
values = self._data
if self._paramstyle == PARAMSTYLE_PYFORMAT:
if not isinstance(self._data, dict):
raise mariadb.ProgrammingError("Data argument must be "
"Dictionary")
for i in range(0, len(self._keys)):
if self._keys[i] not in self._data:
raise mariadb.ProgrammingError("Dictionary doesn't contain"
" key '%s'" % self._keys[i])
values = self._data.values()
else:
# check if number of place holders matches the number of
# supplied elements in data tuple
if self._paramlist and (
(not self._data and len(self._paramlist) > 0) or
(len(self._data) != len(self._paramlist))):
raise mariadb.ProgrammingError(
"statement (%s) doesn't match the number of data elements"
" (%s)." % (len(self._paramlist), len(self._data)))
# CONPY-313: Check if parameter types are supported
if len(values):
for val in values:
if isinstance(val, float) or isinstance(val, decimal.Decimal):
self._check_decimal_parameter(val)
def callproc(self, sp: str, data: Sequence = ()):
"""
Executes a stored procedure sp. The data sequence must contain an
entry for each parameter the procedure expects.
Input/Output or Output parameters have to be retrieved by .fetch
methods, the .sp_outparams attribute indicates if the result set
contains output parameters.
Arguments:
- sp: Name of stored procedure.
- data: Optional sequence containing data for placeholder
substitution.
"""
if self.connection.auto_reconnect:
self._thread_id= self.connection.thread_id
self.check_closed()
self._reset()
# create statement
params = ""
if data and len(data):
params = ("?," * len(data))[:-1]
statement = "CALL %s(%s)" % (sp, params)
self._rowcount = 0
self.execute(statement, data)
def _parse_execute(self, statement: str, data=(), is_bulk=False):
"""
For internal use
Parses SQL statement and checks parameters.
"""
if not statement:
raise mariadb.ProgrammingError("empty statement")
# parse statement
if self.statement != statement or is_bulk and not self._bulk:
super()._parse(statement)
self._prev_stmt = statement
self._reprepare = True
else:
self._reprepare = False
self._transformed_statement = self.statement
if self._cursor_type == CURSOR.READ_ONLY:
self._text = False
self._data = data
self._check_execute_params()
def nextset(self):
"""
Will make the cursor skip to the next available result set,
discarding any remaining rows from the current set.
"""
self.check_closed()
return super()._nextset()
def execute(self, statement: str, data: Sequence = (), buffered=None):
"""
Prepare and execute a SQL statement.
Parameters may be provided as sequence or mapping and will be bound
to variables in the operation. Variables are specified as question
marks (paramstyle ='qmark'), however for compatibility reasons MariaDB
Connector/Python also supports the 'format' and 'pyformat' paramstyles
with the restriction, that different paramstyles can't be mixed within
a statement.
A reference to the operation will be retained by the cursor.
If the cursor was created with attribute prepared =True the statement
string for following execute operations will be ignored.
This is most effective for algorithms where the same operation is used,
but different parameters are bound to it (many times).
By default execute() method generates an buffered result unless the
optional parameter buffered was set to False or the cursor was
generated as an unbuffered cursor.
"""
if self.connection.auto_reconnect:
self._thread_id= self.connection.thread_id
self.check_closed()
if not self._prepared:
self._reset()
self.connection._last_executed_statement = statement
# Parse statement
do_parse = True
self._rowcount = 0
if buffered is not None:
self.buffered = buffered
# clear pending result sets
if self.field_count:
self._clear_result()
# if we have a prepared cursor, we have to set statement
# to previous statement and don't need to parse
if self._prepared and self.statement:
statement = self.statement
do_parse = False
# parse statement and check param style
if do_parse:
self._parse_execute(statement, (data))
self._description = None
# CONPY-218: Allow None as replacement for empty tuple
data = data or ()
if len(data):
self._data = data
else:
self._data = None
# If statement doesn't contain parameters we force to run in text
# mode, unless a server side cursor or stored procedure will be
# executed.
if self._command != SQL_CALL and self._cursor_type == 0:
self._text = True
if self._force_binary:
self._text = False
# if one of the provided parameters has byte or datetime value,
# we don't use text protocol
if data and self._check_text_types() == True:
self._text = False
if self._text:
# in text mode we need to substitute parameters
# and store transformed statement
if (self.paramcount > 0):
self._transformed_statement = self._substitute_parameters()
else:
self._transformed_statement = self.statement
self._execute_text(self._transformed_statement)
self._readresponse()
else:
self._data = data
self._execute_binary()
self._initresult()
self._bulk = 0
def executemany(self, statement, parameters):
"""
Prepare a database operation (INSERT,UPDATE,REPLACE or DELETE
statement) and execute it against all parameter found in sequence.
Exactly behaves like .execute() but accepts a list of tuples, where
each tuple represents data of a row within a table.
.executemany() only supports DML (insert, update, delete) statements.
If the SQL statement contains a RETURNING clause, executemany()
returns a result set containing the values for columns listed in the
RETURNING clause.
"""
if self.connection.auto_reconnect:
self._thread_id= self.connection.thread_id
self.check_closed()
self._reset()
if not parameters or not len(parameters):
raise mariadb.ProgrammingError("No data provided")
self.connection._last_executed_statement = statement
# clear pending results
if self.field_count:
self._clear_result()
# If the server doesn't support bulk operations, we need to emulate
# by looping
# TODO: insert/replace statements are not optimized yet
# rowcount updating
if not (self.connection.extended_server_capabilities &
(CAPABILITY.BULK_OPERATIONS >> 32)):
count = 0
for row in parameters:
self.execute(statement, row)
count += self.rowcount
self._rowcount = count
else:
# parse statement
self._parse_execute(statement, parameters[0], is_bulk=True)
self._data = parameters
self.is_text = False
self._rowcount = 0
self._execute_bulk()
self._bulk = 1
def _fetch_row(self):
"""
Internal use only
fetches row and converts values, if connection has a converter.
"""
if not self.buffered:
self.check_closed()
# if there is no result set, PEP-249 requires to raise an
# exception
if not self.field_count:
raise mariadb.ProgrammingError("Cursor doesn't have a result set")
return super().fetchone()
def close(self):
"""
Closes the cursor.
If the cursor has pending or unread results, .close() will cancel them
so that further operations using the same connection can be executed.
The cursor will be unusable from this point forward; an Error
(or subclass) exception will be raised if any operation is attempted
with the cursor."
"""
# CONPY-231: fix memory leak
if self._data:
del self._data
if not self.connection._closed:
super().close()
self._closed= True
def fetchone(self):
"""
Fetch the next row of a query result set, returning a single sequence,
or None if no more data is available.
An exception will be raised if the previous call to execute() didn't
produce a result set or execute() wasn't called before.
"""
if not (self.buffered and self._text):
self.check_closed()
row = self._fetch_row()
return row
def fetchmany(self, size: int = 0):
"""
Fetch the next set of rows of a query result, returning a sequence
of sequences (e.g. a list of tuples). An empty sequence is returned
when no more rows are available.
The number of rows to fetch per call is specified by the parameter.
If it is not given, the cursor's arraysize determines the number
of rows to be fetched. The method should try to fetch as many rows
as indicated by the size parameter.
If this is not possible due to the specified number of rows not being
available, fewer rows may be returned.
An exception will be raised if the previous call to execute() didn't
produce a result set or execute() wasn't called before.
"""
if not (self.buffered and self._text):
self.check_closed()
if size == 0:
size = self.arraysize
return super().fetchrows(size)
def fetchall(self):
"""
Fetch all remaining rows of a query result, returning them as a
sequence of sequences (e.g. a list of tuples).
An exception will be raised if the previous call to execute() didn't
produce a result set or execute() wasn't called before.
"""
if not (self.buffered and self._text):
self.check_closed()
return super().fetchrows(ROWS_EOF)
def __iter__(self):
return iter(self.fetchone, None)
def scroll(self, value: int, mode="relative"):
"""
Scroll the cursor in the result set to a new position according to
mode.
If mode is "relative" (default), value is taken as offset to the
current position in the result set, if set to absolute, value states
an absolute target position.
"""
if not (self.buffered and self._text):
self.check_closed()
if self.field_count == 0:
raise mariadb.ProgrammingError("Cursor doesn't have a result set")
if not self.buffered:
raise mariadb.ProgrammingError("This method is available only "
"for cursors with a buffered "
"result set.")
if mode != "absolute" and mode != "relative":
raise mariadb.ProgrammingError("Invalid or unknown scroll "
"mode specified.")
if value == 0 and mode != "absolute":
raise mariadb.ProgrammingError("Invalid position value 0.")
if mode == "relative":
if self.rownumber + value < 0 or \
self.rownumber + value > self.rowcount:
raise mariadb.ProgrammingError("Position value "
"is out of range.")
new_pos = self.rownumber + value
else:
if value < 0 or value >= self.rowcount:
raise mariadb.ProgrammingError("Position value "
"is out of range.")
new_pos = value
self._seek(new_pos)
self._rownumber = new_pos
def setinputsizes(self, size: int):
"""
Required by PEP-249. Does nothing in MariaDB Connector/Python
"""
return
def setoutputsize(self, size: int):
"""
Required by PEP-249. Does nothing in MariaDB Connector/Python
"""
return
def __enter__(self):
"""Returns a copy of the cursor."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Closes cursor."""
self.close()
@property
def rowcount(self):
"""
This read-only attribute specifies the number of rows that the last\
execute*() produced (for DQL statements like SELECT) or affected
(for DML statements like UPDATE or INSERT).
The return value is -1 in case no .execute*() has been performed
on the cursor or the rowcount of the last operation cannot be
determined by the interface.
"""
# Even if PEP-249 permits operations on a closed cursor, we don't
# raise an exception if the cursor or the underlying connection
# was closed (See CONPY-269), instead we will return -1
if not self.buffered:
try:
self.check_closed()
except mariadb.ProgrammingError:
return -1
if self._rowcount > 0:
return self._rowcount
return super().rowcount
@property
def sp_outparams(self):
"""
Indicates if the current result set contains in out or out parameter
from a previous executed stored procedure
"""
self.check_closed()
return bool(self.connection.server_status & STATUS.PS_OUT_PARAMS)
@property
def lastrowid(self):
"""
Returns the ID generated by a query on a table with a column having
the AUTO_INCREMENT attribute or the value for the last usage of
LAST_INSERT_ID().
If the last query wasn't an INSERT or UPDATE
statement or if the modified table does not have a column with the
AUTO_INCREMENT attribute and LAST_INSERT_ID was not used, the returned
value will be None
"""
self.check_closed()
id = self.insert_id
if id > 0:
return id
return None
@property
def connection(self):
"""
Read-Only attribute which returns the reference to the connection
object on which the cursor was created.
"""
return self._connection
@@ -0,0 +1,111 @@
from mariadb.constants import FIELD_TYPE
import time
import datetime
apilevel = '2.0'
paramstyle = 'qmark'
threadsafety = True
class DbApiType(frozenset):
"""
Immutable set for type checking
By default the following sets are defined:
- BINARY: for binary field types
- NUMBER: for numeric field types
- STRING: for character based (string) field types
- DATE: for date field type(s)
- DATETIME: for datetime and timestamp field type(s)
- TIME: for time field type(s)
- TIMESTAMP: for datetime and timestamp field type(s)
Example:
>>> FIELD_TYPE.GEOMETRY == mariadb.BINARY
True
>>> FIELD_TYPE.FLOAT == mariadb.BINARY
False
"""
def __eq__(self, field_type):
if (isinstance(field_type, DbApiType)):
return not self.difference(field_type)
return field_type in self
BINARY = DbApiType([FIELD_TYPE.GEOMETRY,
FIELD_TYPE.LONG_BLOB,
FIELD_TYPE.MEDIUM_BLOB,
FIELD_TYPE.TINY_BLOB,
FIELD_TYPE.BLOB])
STRING = DbApiType([FIELD_TYPE.ENUM,
FIELD_TYPE.JSON,
FIELD_TYPE.STRING,
FIELD_TYPE.VARCHAR,
FIELD_TYPE.VAR_STRING])
NUMBER = DbApiType([FIELD_TYPE.DECIMAL,
FIELD_TYPE.DOUBLE,
FIELD_TYPE.FLOAT,
FIELD_TYPE.INT24,
FIELD_TYPE.LONG,
FIELD_TYPE.LONGLONG,
FIELD_TYPE.NEWDECIMAL,
FIELD_TYPE.SHORT,
FIELD_TYPE.TINY,
FIELD_TYPE.YEAR])
DATE = DbApiType([FIELD_TYPE.DATE])
TIME = DbApiType([FIELD_TYPE.TIME])
DATETIME = TIMESTAMP = DbApiType([FIELD_TYPE.DATETIME,
FIELD_TYPE.TIMESTAMP])
ROWID = DbApiType()
def Binary(object):
"""Constructs an object capable of holding a binary value."""
return bytes(object)
def Date(year, month, day):
"""Constructs an object holding a date value."""
return datetime.date(year, month, day)
def Time(hour, minute, second):
"""Constructs an object holding a time value."""
return datetime.time(hour, minute, second)
def Timestamp(year, month, day, hour, minute, second):
"""Constructs an object holding a datetime value."""
return datetime.datetime(year, month, day, hour, minute, second)
def DateFromTicks(ticks):
"""Constructs an object holding a date value from the given ticks value
(number of seconds since the epoch).
For more information see the documentation of the standard Python
time module."""
return Date(*time.localtime(ticks)[:3])
def TimeFromTicks(ticks):
"""Constructs an object holding a time value from the given ticks value
(number of seconds since the epoch).
For more information see the documentation of the standard Python
time module."""
return Time(*time.localtime(ticks)[3:6])
def TimestampFromTicks(ticks):
"""Constructs an object holding a datetime value from the given ticks value
(number of seconds since the epoch).
For more information see the documentation of the standard Python
time module."""
return datetime.datetime(*time.localtime(ticks)[:6])
@@ -0,0 +1,59 @@
from mariadb.constants import FIELD_TYPE, FIELD_FLAG
field_types = {FIELD_TYPE.DECIMAL: "DECIMAL",
FIELD_TYPE.TINY: "TINY",
FIELD_TYPE.SHORT: "SHORT",
FIELD_TYPE.LONG: "LONG",
FIELD_TYPE.FLOAT: "FLOAT",
FIELD_TYPE.DOUBLE: "DOUBLE",
FIELD_TYPE.NULL: "NULL",
FIELD_TYPE.TIMESTAMP: "TIMESTAMP",
FIELD_TYPE.LONGLONG: "LONGLONG",
FIELD_TYPE.INT24: "INT24",
FIELD_TYPE.DATE: "DATE",
FIELD_TYPE.TIME: "TIME",
FIELD_TYPE.DATETIME: "DATETIME",
FIELD_TYPE.YEAR: "YEAR",
FIELD_TYPE.NEWDATE: "NEWDATE",
FIELD_TYPE.VARCHAR: "VARCHAR",
FIELD_TYPE.BIT: "BIT",
FIELD_TYPE.JSON: "JSON",
FIELD_TYPE.NEWDECIMAL: "NEWDECIMAL",
FIELD_TYPE.ENUM: "ENUM",
FIELD_TYPE.SET: "SET",
FIELD_TYPE.TINY_BLOB: "TINY_BLOB",
FIELD_TYPE.MEDIUM_BLOB: "MEDIUM_BLOB",
FIELD_TYPE.LONG_BLOB: "LONG_BLOB",
FIELD_TYPE.BLOB: "BLOB",
FIELD_TYPE.VAR_STRING: "VAR_STRING",
FIELD_TYPE.STRING: "STRING",
FIELD_TYPE.GEOMETRY: "GEOMETRY"}
field_flags = {FIELD_FLAG.NOT_NULL: "NOT_NULL",
FIELD_FLAG.PRIMARY_KEY: "PRIMARY_KEY",
FIELD_FLAG.UNIQUE_KEY: "UNIQUE_KEY",
FIELD_FLAG.MULTIPLE_KEY: "PART_KEY",
FIELD_FLAG.BLOB: "BLOB",
FIELD_FLAG.UNSIGNED: "UNSIGNED",
FIELD_FLAG.ZEROFILL: "ZEROFILL",
FIELD_FLAG.BINARY: "BINARY",
FIELD_FLAG.ENUM: "NUMERIC",
FIELD_FLAG.AUTO_INCREMENT: "AUTO_INCREMENT",
FIELD_FLAG.TIMESTAMP: "TIMESTAMP",
FIELD_FLAG.SET: "SET",
FIELD_FLAG.NO_DEFAULT: "NO_DEFAULT",
FIELD_FLAG.ON_UPDATE_NOW: "UPDATE_TIMESTAMP",
FIELD_FLAG.NUMERIC: "NUMERIC"}
class fieldinfo():
def type(self, description):
if description[1] in field_types:
return field_types[description[1]]
return None
def flag(self, description):
flags = [field_flags[f] for f in field_flags.keys()
if description[7] & f]
return " | ".join(flags)
@@ -0,0 +1,3 @@
__author__ = 'Georg Richter'
__version__ = '1.1.14'
__version_info__ = (1, 1, 14)
@@ -0,0 +1,112 @@
Metadata-Version: 2.4
Name: packaging
Version: 26.3
Summary: Core utilities for Python packages
Author-email: Donald Stufft <donald@stufft.io>
Requires-Python: >=3.9
Description-Content-Type: text/x-rst
License-Expression: Apache-2.0 OR BSD-2-Clause
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Programming Language :: Python :: Free Threading :: 4 - Resilient
Classifier: Typing :: Typed
License-File: LICENSE
License-File: LICENSE.APACHE
License-File: LICENSE.BSD
Project-URL: Documentation, https://packaging.pypa.io/
Project-URL: Source, https://github.com/pypa/packaging
packaging
=========
.. start-intro
Reusable core utilities for various Python Packaging
`interoperability specifications <https://packaging.python.org/specifications/>`_.
This library provides utilities that implement the interoperability
specifications which have clearly one correct behaviour (eg: :pep:`440`)
or benefit greatly from having a single shared implementation (eg: :pep:`425`).
.. end-intro
The ``packaging`` project includes the following: version handling, specifiers,
markers, requirements, tags, metadata, lockfiles, utilities.
Documentation
-------------
The `documentation`_ provides information and the API for the following:
- Version Handling
- Specifiers
- Markers
- Licenses
- Requirements
- Metadata
- Tags
- Lockfiles (pylock)
- Direct URL helpers
- Dependency groups
- Errors
- Utilities
Installation
------------
Use ``pip`` to install these utilities::
pip install packaging
The ``packaging`` library uses calendar-based versioning (``YY.N``).
Discussion
----------
If you run into bugs, you can file them in our `issue tracker`_.
You can also join discussions on `GitHub Discussions`_ to ask questions or get involved.
.. _`documentation`: https://packaging.pypa.io/
.. _`issue tracker`: https://github.com/pypa/packaging/issues
.. _`GitHub Discussions`: https://github.com/pypa/packaging/discussions
Code of Conduct
---------------
Everyone interacting in the packaging project's codebases, issue trackers, chat
rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
Contributing
------------
The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as
well as how to report a potential security issue. The documentation for this
project also covers information about `project development`_ and `security`_.
.. _`project development`: https://packaging.pypa.io/en/latest/development/
.. _`security`: https://packaging.pypa.io/en/latest/security/
Project History
---------------
Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for
recent changes and project history.
.. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/
@@ -0,0 +1,52 @@
packaging-26.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
packaging-26.3.dist-info/METADATA,sha256=cP24n8TUqaBDv3NyuJcrzIg_3f80q1Xpz4DXOHU4R2M,3544
packaging-26.3.dist-info/RECORD,,
packaging-26.3.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
packaging-26.3.dist-info/licenses/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
packaging-26.3.dist-info/licenses/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
packaging-26.3.dist-info/licenses/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
packaging/__init__.py,sha256=bnWM3QAossrXTHObmtycnwbwVJMjBigLh4v-WdOSGbU,494
packaging/__pycache__/__init__.cpython-312.pyc,,
packaging/__pycache__/_elffile.cpython-312.pyc,,
packaging/__pycache__/_manylinux.cpython-312.pyc,,
packaging/__pycache__/_musllinux.cpython-312.pyc,,
packaging/__pycache__/_parser.cpython-312.pyc,,
packaging/__pycache__/_ranges.cpython-312.pyc,,
packaging/__pycache__/_structures.cpython-312.pyc,,
packaging/__pycache__/_tokenizer.cpython-312.pyc,,
packaging/__pycache__/dependency_groups.cpython-312.pyc,,
packaging/__pycache__/direct_url.cpython-312.pyc,,
packaging/__pycache__/errors.cpython-312.pyc,,
packaging/__pycache__/markers.cpython-312.pyc,,
packaging/__pycache__/metadata.cpython-312.pyc,,
packaging/__pycache__/pylock.cpython-312.pyc,,
packaging/__pycache__/ranges.cpython-312.pyc,,
packaging/__pycache__/requirements.cpython-312.pyc,,
packaging/__pycache__/specifiers.cpython-312.pyc,,
packaging/__pycache__/tags.cpython-312.pyc,,
packaging/__pycache__/utils.cpython-312.pyc,,
packaging/__pycache__/version.cpython-312.pyc,,
packaging/_elffile.py,sha256=HwJfVMVz8ahXQMT72QjMHR-wwXfBQ3fr9fCdbNU2h4A,3236
packaging/_manylinux.py,sha256=W26EMkxCU4q0zbFI4kMP26NvBEJ7Dv2lW-oIOjX6YTY,10000
packaging/_musllinux.py,sha256=Ayj7gnsRcMd99MEH1Jvo3403JiEDSQph6Kytii-crf8,2774
packaging/_parser.py,sha256=iKNIEVIJMnSX5FO2NeXbCZ_VkvkzXXEMcxOynNswxQw,12639
packaging/_ranges.py,sha256=t7WV8uaSrrNpfFRbhcJMu8tFUvKtxtkFmqgtCHfoFrc,30850
packaging/_structures.py,sha256=60jRbF78p8z5MKnNd6cAprgOadCJHV0DlmUmRBqFZcs,1109
packaging/_tokenizer.py,sha256=zCXlfA1WTInhUHNHHW4cJkKZypRvsXwOHkr1TvdxPuY,5573
packaging/dependency_groups.py,sha256=UwnMVkyjAi37WUgW6dWTuwnN80IIwfuMXsmvtsZwJ4w,11224
packaging/direct_url.py,sha256=hDuqldBMu0kojenZ_XfulG6GLLYDEj5-RqaeFx-n0Gs,11645
packaging/errors.py,sha256=6hfEYXAf8v_IF65-lFadJOMIieBP2xIKtyEXjG1nGIs,2680
packaging/licenses/__init__.py,sha256=WQ0S7uc92-xTtOnuW_xk2cs6rLSUDo68uiYHV85WdAs,7859
packaging/licenses/__pycache__/__init__.cpython-312.pyc,,
packaging/licenses/__pycache__/_spdx.cpython-312.pyc,,
packaging/licenses/_spdx.py,sha256=WW7DXiyg68up_YND_wpRYlr1SHhiV4FfJLQffghhMxQ,51122
packaging/markers.py,sha256=bL6GDTXZjStZnn8Oxz_qsSTgFXEbML6QReZ4ySlA2cw,20554
packaging/metadata.py,sha256=k0FJ9CClRJ5vN2qmHW8moII-uCzHxqHIzeWqe-Q9D2M,42059
packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
packaging/pylock.py,sha256=G84Kn-Y-Qrh-jO0F2icrrxamiqcEs3GEBeJzWmESRnw,35232
packaging/ranges.py,sha256=tHgc6RPl3_ngeQbyBYXFTDAo4ff_VjQYKOedUi3b-rY,83054
packaging/requirements.py,sha256=RZR26yEH_jDmsTEAUaW9tEx9O3944Oh7raJoKCy6YvU,7040
packaging/specifiers.py,sha256=ZhrTHd9F_7hh0mJnNPTj1mlmApuP23lYa7EFgl2NnVk,52798
packaging/tags.py,sha256=DnF6CCp5Yg15zwMhtCDfFVk8SRb82gPCfLnYLgfvXW4,38302
packaging/utils.py,sha256=oTdCzZmU7spa37AknRJkCMR4Am6x9Iy5evvy_p8o7MY,12215
packaging/version.py,sha256=VYsvtQ_RmMZgpMCC-WuJGcVlkRtrS24H80trYMB0vpc,39040
@@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: flit 3.12.0
Root-Is-Purelib: true
Tag: py3-none-any
@@ -0,0 +1,3 @@
This software is made available under the terms of *either* of the licenses
found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made
under the terms of *both* these licenses.
@@ -0,0 +1,177 @@
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
@@ -0,0 +1,23 @@
Copyright (c) Donald Stufft 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.
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.
@@ -0,0 +1,15 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
__title__ = "packaging"
__summary__ = "Core utilities for Python packages"
__uri__ = "https://github.com/pypa/packaging"
__version__ = "26.3"
__author__ = "Donald Stufft and individual contributors"
__email__ = "donald@stufft.io"
__license__ = "BSD-2-Clause or Apache-2.0"
__copyright__ = f"2014 {__author__}"
@@ -0,0 +1,108 @@
"""
ELF file parser.
This provides a class ``ELFFile`` that parses an ELF executable in a similar
interface to ``ZipFile``. Only the read interface is implemented.
ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
"""
from __future__ import annotations
import enum
import os
import struct
from typing import IO
class ELFInvalid(ValueError):
pass
class EIClass(enum.IntEnum):
C32 = 1
C64 = 2
class EIData(enum.IntEnum):
Lsb = 1
Msb = 2
class EMachine(enum.IntEnum):
I386 = 3
S390 = 22
Arm = 40
X8664 = 62
AArch64 = 183
class ELFFile:
"""
Representation of an ELF executable.
"""
def __init__(self, f: IO[bytes]) -> None:
self._f = f
try:
ident = self._read("16B")
except struct.error as e:
raise ELFInvalid("unable to parse identification") from e
magic = bytes(ident[:4])
if magic != b"\x7fELF":
raise ELFInvalid(f"invalid magic: {magic!r}")
self.capacity = ident[4] # Format for program header (bitness).
self.encoding = ident[5] # Data structure encoding (endianness).
try:
# e_fmt: Format for the ELF header.
# p_fmt: Format for a program header.
# p_idx: Indexes to find p_type, p_offset, and p_filesz.
e_fmt, self._p_fmt, self._p_idx = {
(1, 1): ("<HHIIIIIHHH", "<IIIIIIII", (0, 1, 4)), # 32-bit LSB.
(1, 2): (">HHIIIIIHHH", ">IIIIIIII", (0, 1, 4)), # 32-bit MSB.
(2, 1): ("<HHIQQQIHHH", "<IIQQQQQQ", (0, 2, 5)), # 64-bit LSB.
(2, 2): (">HHIQQQIHHH", ">IIQQQQQQ", (0, 2, 5)), # 64-bit MSB.
}[(self.capacity, self.encoding)]
except KeyError as e:
raise ELFInvalid(
f"unrecognized capacity ({self.capacity}) or encoding ({self.encoding})"
) from e
try:
(
_,
self.machine, # Architecture type.
_,
_,
self._e_phoff, # Offset of program header.
_,
self.flags, # Processor-specific flags.
_,
self._e_phentsize, # Size of a program header entry.
self._e_phnum, # Number of program headers.
) = self._read(e_fmt)
except struct.error as e:
raise ELFInvalid("unable to parse machine and section information") from e
def _read(self, fmt: str) -> tuple[int, ...]:
return struct.unpack(fmt, self._f.read(struct.calcsize(fmt)))
@property
def interpreter(self) -> str | None:
"""
The path recorded in the ``PT_INTERP`` section header.
"""
for index in range(self._e_phnum):
self._f.seek(self._e_phoff + self._e_phentsize * index)
try:
data = self._read(self._p_fmt)
except struct.error:
continue
if data[self._p_idx[0]] != 3: # Not PT_INTERP.
continue
self._f.seek(data[self._p_idx[1]])
return os.fsdecode(self._f.read(data[self._p_idx[2]])).strip("\0")
return None
@@ -0,0 +1,278 @@
from __future__ import annotations
import collections
import contextlib
import functools
import os
import re
import sys
import warnings
from typing import TYPE_CHECKING, NamedTuple
from ._elffile import EIClass, EIData, ELFFile, EMachine
if TYPE_CHECKING:
import types
from collections.abc import Generator, Iterator, Sequence
EF_ARM_ABIMASK = 0xFF000000
EF_ARM_ABI_VER5 = 0x05000000
EF_ARM_ABI_FLOAT_HARD = 0x00000400
_ALLOWED_ARCHS = {
"x86_64",
"aarch64",
"ppc64",
"ppc64le",
"s390x",
"loongarch64",
"riscv64",
}
@contextlib.contextmanager
def _parse_elf(path: str) -> Generator[ELFFile | None, None, None]:
try:
with open(path, "rb") as f:
yield ELFFile(f)
except (OSError, TypeError, ValueError):
yield None
def _is_linux_armhf(executable: str) -> bool:
# hard-float ABI can be detected from the ELF header of the running
# process
# https://static.docs.arm.com/ihi0044/g/aaelf32.pdf
with _parse_elf(executable) as f:
return (
f is not None
and f.capacity == EIClass.C32
and f.encoding == EIData.Lsb
and f.machine == EMachine.Arm
and f.flags & EF_ARM_ABIMASK == EF_ARM_ABI_VER5
and f.flags & EF_ARM_ABI_FLOAT_HARD == EF_ARM_ABI_FLOAT_HARD
)
def _is_linux_i686(executable: str) -> bool:
with _parse_elf(executable) as f:
return (
f is not None
and f.capacity == EIClass.C32
and f.encoding == EIData.Lsb
and f.machine == EMachine.I386
)
def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
if "armv7l" in archs:
return _is_linux_armhf(executable)
if "i686" in archs:
return _is_linux_i686(executable)
return any(arch in _ALLOWED_ARCHS for arch in archs)
# If glibc ever changes its major version, we need to know what the last
# minor version was, so we can build the complete list of all versions.
# For now, guess what the highest minor version might be, assume it will
# be 50 for testing. Once this actually happens, update the dictionary
# with the actual value.
_LAST_GLIBC_MINOR: dict[int, int] = collections.defaultdict(lambda: 50)
class _GLibCVersion(NamedTuple):
major: int
minor: int
def _glibc_version_string_confstr() -> str | None:
"""
Primary implementation of glibc_version_string using os.confstr.
"""
# os.confstr is quite a bit faster than ctypes.DLL. It's also less likely
# to be broken or missing. This strategy is used in the standard library
# platform module.
# https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183
try:
# Should be a string like "glibc 2.17".
version_string: str | None = os.confstr("CS_GNU_LIBC_VERSION")
assert version_string is not None
_, version = version_string.rsplit()
except (AssertionError, AttributeError, OSError, ValueError):
# os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)...
return None
return version
def _glibc_version_string_ctypes() -> str | None:
"""
Fallback implementation of glibc_version_string using ctypes.
"""
try:
import ctypes # noqa: PLC0415
except ImportError:
return None
# ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen
# manpage says, "If filename is NULL, then the returned handle is for the
# main program". This way we can let the linker do the work to figure out
# which libc our process is actually using.
#
# We must also handle the special case where the executable is not a
# dynamically linked executable. This can occur when using musl libc,
# for example. In this situation, dlopen() will error, leading to an
# OSError. Interestingly, at least in the case of musl, there is no
# errno set on the OSError. The single string argument used to construct
# OSError comes from libc itself and is therefore not portable to
# hard code here. In any case, failure to call dlopen() means we
# can proceed, so we bail on our attempt.
try:
process_namespace = ctypes.CDLL(None)
except OSError:
return None
try:
gnu_get_libc_version = process_namespace.gnu_get_libc_version
except AttributeError:
# Symbol doesn't exist -> therefore, we are not linked to
# glibc.
return None
# Call gnu_get_libc_version, which returns a string like "2.5".
gnu_get_libc_version.restype = ctypes.c_char_p
# A c_char_p restype comes back as bytes, so decode to text.
version_str: str | bytes = gnu_get_libc_version()
if isinstance(version_str, bytes):
version_str = version_str.decode("ascii")
return version_str
def _glibc_version_string() -> str | None:
"""Returns glibc version string, or None if not using glibc."""
return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
def _parse_glibc_version(version_str: str) -> _GLibCVersion:
"""Parse glibc version.
We use a regexp instead of str.split because we want to discard any
random junk that might come after the minor version -- this might happen
in patched/forked versions of glibc (e.g. Linaro's version of glibc
uses version strings like "2.20-2014.11"). See gh-3588.
"""
m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str)
if not m:
warnings.warn(
f"Expected glibc version with 2 components major.minor, got: {version_str}",
RuntimeWarning,
stacklevel=2,
)
return _GLibCVersion(-1, -1)
return _GLibCVersion(int(m.group("major")), int(m.group("minor")))
@functools.lru_cache
def _get_glibc_version() -> _GLibCVersion:
version_str = _glibc_version_string()
if version_str is None:
return _GLibCVersion(-1, -1)
return _parse_glibc_version(version_str)
# From PEP 513, PEP 600
@functools.lru_cache(maxsize=1)
def _get_manylinux_module() -> types.ModuleType | None:
"""Return the ``_manylinux`` C extension module, or None if unavailable.
The result is cached for the lifetime of the process, since the presence
of the module does not change while running.
"""
try:
return __import__("_manylinux")
except ImportError:
return None
def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
sys_glibc = _get_glibc_version()
if sys_glibc < version:
return False
# Check for presence of _manylinux module.
manylinux_mod = _get_manylinux_module()
if manylinux_mod is None:
return True
if hasattr(manylinux_mod, "manylinux_compatible"):
result = manylinux_mod.manylinux_compatible(version[0], version[1], arch)
if result is not None:
return bool(result)
return True
if version == _GLibCVersion(2, 5) and hasattr(
manylinux_mod, "manylinux1_compatible"
):
return bool(manylinux_mod.manylinux1_compatible)
if version == _GLibCVersion(2, 12) and hasattr(
manylinux_mod, "manylinux2010_compatible"
):
return bool(manylinux_mod.manylinux2010_compatible)
if version == _GLibCVersion(2, 17) and hasattr(
manylinux_mod, "manylinux2014_compatible"
):
return bool(manylinux_mod.manylinux2014_compatible)
return True
_LEGACY_MANYLINUX_MAP: dict[_GLibCVersion, str] = {
# CentOS 7 w/ glibc 2.17 (PEP 599)
_GLibCVersion(2, 17): "manylinux2014",
# CentOS 6 w/ glibc 2.12 (PEP 571)
_GLibCVersion(2, 12): "manylinux2010",
# CentOS 5 w/ glibc 2.5 (PEP 513)
_GLibCVersion(2, 5): "manylinux1",
}
def platform_tags(archs: Sequence[str]) -> Iterator[str]:
"""Generate manylinux tags compatible to the current platform.
:param archs: Sequence of compatible architectures.
The first one shall be the closest to the actual architecture and be the part of
platform tag after the ``linux_`` prefix, e.g. ``x86_64``.
The ``linux_`` prefix is assumed as a prerequisite for the current platform to
be manylinux-compatible.
:returns: An iterator of compatible manylinux tags.
"""
if not _have_compatible_abi(sys.executable, archs):
return
# Oldest glibc to be supported regardless of architecture is (2, 17).
too_old_glibc2 = _GLibCVersion(2, 16)
if set(archs) & {"x86_64", "i686"}:
# On x86/i686 also oldest glibc to be supported is (2, 5).
too_old_glibc2 = _GLibCVersion(2, 4)
current_glibc = _GLibCVersion(*_get_glibc_version())
glibc_max_list = [current_glibc]
# We can assume compatibility across glibc major versions.
# https://sourceware.org/bugzilla/show_bug.cgi?id=24636
#
# Build a list of maximum glibc versions so that we can
# output the canonical list of all glibc from current_glibc
# down to too_old_glibc2, including all intermediary versions.
for glibc_major in range(current_glibc.major - 1, 1, -1):
glibc_minor = _LAST_GLIBC_MINOR[glibc_major]
glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor))
for arch in archs:
for glibc_max in glibc_max_list:
if glibc_max.major == too_old_glibc2.major:
min_minor = too_old_glibc2.minor
else:
# For other glibc major versions oldest supported is (x, 0).
min_minor = -1
for glibc_minor in range(glibc_max.minor, min_minor, -1):
glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
if _is_compatible(arch, glibc_version):
yield "manylinux_{}_{}_{}".format(*glibc_version, arch)
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
if legacy_tag := _LEGACY_MANYLINUX_MAP.get(glibc_version):
yield f"{legacy_tag}_{arch}"
@@ -0,0 +1,88 @@
"""PEP 656 support.
This module implements logic to detect if the currently running Python is
linked against musl, and what musl version is used.
"""
from __future__ import annotations
import functools
import re
import subprocess
import sys
from typing import TYPE_CHECKING, NamedTuple
from ._elffile import ELFFile
if TYPE_CHECKING:
from collections.abc import Iterator, Sequence
class _MuslVersion(NamedTuple):
major: int
minor: int
def _parse_musl_version(output: str) -> _MuslVersion | None:
lines = [n for n in (n.strip() for n in output.splitlines()) if n]
if len(lines) < 2 or lines[0][:4] != "musl":
return None
m = re.match(r"Version (\d+)\.(\d+)", lines[1])
if not m:
return None
return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2)))
@functools.lru_cache
def _get_musl_version(executable: str) -> _MuslVersion | None:
"""Detect currently-running musl runtime version.
This is done by checking the specified executable's dynamic linking
information, and invoking the loader to parse its output for a version
string. If the loader is musl, the output would be something like::
musl libc (x86_64)
Version 1.2.2
Dynamic Program Loader
"""
try:
with open(executable, "rb") as f:
ld = ELFFile(f).interpreter
except (OSError, TypeError, ValueError):
return None
if ld is None or "musl" not in ld:
return None
proc = subprocess.run([ld], check=False, stderr=subprocess.PIPE, text=True)
return _parse_musl_version(proc.stderr)
def platform_tags(archs: Sequence[str]) -> Iterator[str]:
"""Generate musllinux tags compatible to the current platform.
:param archs: Sequence of compatible architectures.
The first one shall be the closest to the actual architecture and be the part of
platform tag after the ``linux_`` prefix, e.g. ``x86_64``.
The ``linux_`` prefix is assumed as a prerequisite for the current platform to
be musllinux-compatible.
:returns: An iterator of compatible musllinux tags.
"""
sys_musl = _get_musl_version(sys.executable)
if sys_musl is None: # Python not dynamically linked against musl.
return
for arch in archs:
for minor in range(sys_musl.minor, -1, -1):
yield f"musllinux_{sys_musl.major}_{minor}_{arch}"
if __name__ == "__main__": # pragma: no cover
import sysconfig
plat = sysconfig.get_platform()
assert plat.startswith("linux-"), "not linux"
print("plat:", plat)
print("musl:", _get_musl_version(sys.executable))
print("tags:", end=" ")
for t in platform_tags([re.sub(r"[.-]", "_", plat.split("-", 1)[-1])]):
print(t, end="\n ")
@@ -0,0 +1,418 @@
"""Handwritten parser of dependency specifiers.
The docstring for each __parse_* function contains EBNF-inspired grammar representing
the implementation.
"""
from __future__ import annotations
import ast
from collections.abc import Sequence
from typing import Literal, NamedTuple, Union
from ._tokenizer import DEFAULT_RULES, ParserSyntaxError, Tokenizer
class Node:
__slots__ = ("value",)
def __init__(self, value: str) -> None:
self.value = value
def __str__(self) -> str:
return self.value
def __repr__(self) -> str:
return f"<{self.__class__.__name__}({self.value!r})>"
def serialize(self) -> str:
raise NotImplementedError
def __getstate__(self) -> str:
# Return just the value string for compactness and stability.
return self.value
def _restore_value(self, value: object) -> None:
if not isinstance(value, str):
raise TypeError(
f"Cannot restore {self.__class__.__name__} value from {value!r}"
)
self.value = value
def __setstate__(self, state: object) -> None:
if isinstance(state, str):
# New format (26.2+): just the value string.
self._restore_value(state)
return
if isinstance(state, tuple) and len(state) == 2:
# Old format (packaging <= 26.0, __slots__): (None, {slot: value}).
_, slot_dict = state
if isinstance(slot_dict, dict) and "value" in slot_dict:
self._restore_value(slot_dict["value"])
return
if isinstance(state, dict) and "value" in state:
# Old format (packaging <= 25.0, no __slots__): plain __dict__.
self._restore_value(state["value"])
return
raise TypeError(f"Cannot restore {self.__class__.__name__} from {state!r}")
class Variable(Node):
__slots__ = ()
def serialize(self) -> str:
return str(self)
class Value(Node):
__slots__ = ()
def serialize(self) -> str:
value = str(self)
if '"' not in value:
return f'"{value}"'
if "'" not in value:
return f"'{value}'"
raise ValueError(
"Cannot serialize marker value containing both quote characters"
)
class Op(Node):
__slots__ = ()
def serialize(self) -> str:
return str(self)
MarkerLogical = Literal["and", "or"]
MarkerVar = Union[Variable, Value]
MarkerItem = tuple[MarkerVar, Op, MarkerVar]
MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]
MarkerList = list[Union["MarkerList", MarkerAtom, MarkerLogical]]
class ParsedRequirement(NamedTuple):
name: str
url: str
extras: list[str]
specifier: str
marker: MarkerList | None
# --------------------------------------------------------------------------------------
# Recursive descent parser for dependency specifier
# --------------------------------------------------------------------------------------
def parse_requirement(source: str) -> ParsedRequirement:
return _parse_requirement(Tokenizer(source, rules=DEFAULT_RULES))
def _parse_requirement(tokenizer: Tokenizer) -> ParsedRequirement:
"""
requirement = WS? IDENTIFIER WS? extras WS? requirement_details
"""
tokenizer.consume("WS")
name_token = tokenizer.expect(
"IDENTIFIER", expected="package name at the start of dependency specifier"
)
name = name_token.text
tokenizer.consume("WS")
extras = _parse_extras(tokenizer)
tokenizer.consume("WS")
url, specifier, marker = _parse_requirement_details(tokenizer)
tokenizer.expect("END", expected="end of dependency specifier")
return ParsedRequirement(name, url, extras, specifier, marker)
def _parse_requirement_details(
tokenizer: Tokenizer,
) -> tuple[str, str, MarkerList | None]:
"""
requirement_details = AT URL (WS requirement_marker?)?
| specifier WS? (requirement_marker)?
"""
specifier = ""
url = ""
marker = None
if tokenizer.check("AT"):
tokenizer.read()
tokenizer.consume("WS")
url_start = tokenizer.position
url = tokenizer.expect("URL", expected="URL after @").text
if tokenizer.check("END", peek=True):
return (url, specifier, marker)
tokenizer.expect("WS", expected="whitespace after URL")
# The input might end after whitespace.
if tokenizer.check("END", peek=True):
return (url, specifier, marker)
marker = _parse_requirement_marker(
tokenizer,
span_start=url_start,
expected="semicolon (after URL and whitespace)",
)
else:
specifier_start = tokenizer.position
specifier = _parse_specifier(tokenizer)
tokenizer.consume("WS")
if tokenizer.check("END", peek=True):
return (url, specifier, marker)
marker = _parse_requirement_marker(
tokenizer,
span_start=specifier_start,
expected=(
"comma (within version specifier), semicolon (after version specifier)"
if specifier
else "semicolon (after name with no version specifier)"
),
)
return (url, specifier, marker)
def _parse_requirement_marker(
tokenizer: Tokenizer, *, span_start: int, expected: str
) -> MarkerList:
"""
requirement_marker = SEMICOLON marker WS?
"""
if not tokenizer.check("SEMICOLON"):
tokenizer.raise_syntax_error(
f"Expected {expected} or end",
span_start=span_start,
span_end=None,
)
tokenizer.read()
marker = _parse_marker(tokenizer)
tokenizer.consume("WS")
return marker
def _parse_extras(tokenizer: Tokenizer) -> list[str]:
"""
extras = (LEFT_BRACKET wsp* extras_list? wsp* RIGHT_BRACKET)?
"""
if not tokenizer.check("LEFT_BRACKET", peek=True):
return []
with tokenizer.enclosing_tokens(
"LEFT_BRACKET",
"RIGHT_BRACKET",
around="extras",
):
tokenizer.consume("WS")
extras = _parse_extras_list(tokenizer)
tokenizer.consume("WS")
return extras
def _parse_extras_list(tokenizer: Tokenizer) -> list[str]:
"""
extras_list = identifier (wsp* ',' wsp* identifier)*
"""
extras: list[str] = []
if not tokenizer.check("IDENTIFIER"):
return extras
extras.append(tokenizer.read().text)
while True:
tokenizer.consume("WS")
if tokenizer.check("IDENTIFIER", peek=True):
tokenizer.raise_syntax_error("Expected comma between extra names")
elif not tokenizer.check("COMMA"):
break
tokenizer.read()
tokenizer.consume("WS")
extra_token = tokenizer.expect("IDENTIFIER", expected="extra name after comma")
extras.append(extra_token.text)
return extras
def _parse_specifier(tokenizer: Tokenizer) -> str:
"""
specifier = LEFT_PARENTHESIS WS? version_many WS? RIGHT_PARENTHESIS
| WS? version_many WS?
"""
with tokenizer.enclosing_tokens(
"LEFT_PARENTHESIS",
"RIGHT_PARENTHESIS",
around="version specifier",
):
tokenizer.consume("WS")
parsed_specifiers = _parse_version_many(tokenizer)
tokenizer.consume("WS")
return parsed_specifiers
def _parse_version_many(tokenizer: Tokenizer) -> str:
"""
version_many = (SPECIFIER (WS? COMMA WS? SPECIFIER)*)?
"""
parsed_specifiers = ""
while tokenizer.check("SPECIFIER"):
span_start = tokenizer.position
specifier = tokenizer.read().text
parsed_specifiers += specifier
if tokenizer.check("VERSION_PREFIX_TRAIL", peek=True):
message = ".* suffix can only be used with `==` or `!=` operators"
if specifier.startswith("!=") or (
specifier.startswith("==") and not specifier.startswith("===")
):
message = (
".* suffix cannot be used with pre-release, post-release, "
"dev or local versions"
)
tokenizer.raise_syntax_error(
message,
span_start=span_start,
span_end=tokenizer.position + 1,
)
if tokenizer.check("VERSION_LOCAL_LABEL_TRAIL", peek=True):
tokenizer.raise_syntax_error(
"Local version label can only be used with `==` or `!=` operators",
span_start=span_start,
span_end=tokenizer.position,
)
tokenizer.consume("WS")
if not tokenizer.check("COMMA"):
break
parsed_specifiers += tokenizer.read().text
tokenizer.consume("WS")
return parsed_specifiers
# --------------------------------------------------------------------------------------
# Recursive descent parser for marker expression
# --------------------------------------------------------------------------------------
def parse_marker(source: str) -> MarkerList:
return _parse_full_marker(Tokenizer(source, rules=DEFAULT_RULES))
def _parse_full_marker(tokenizer: Tokenizer) -> MarkerList:
retval = _parse_marker(tokenizer)
tokenizer.expect("END", expected="end of marker expression")
return retval
def _parse_marker(tokenizer: Tokenizer) -> MarkerList:
"""
marker = marker_atom (BOOLOP marker_atom)+
"""
expression = [_parse_marker_atom(tokenizer)]
while tokenizer.check("BOOLOP"):
token = tokenizer.read()
expr_right = _parse_marker_atom(tokenizer)
expression.extend((token.text, expr_right))
return expression
def _parse_marker_atom(tokenizer: Tokenizer) -> MarkerAtom:
"""
marker_atom = WS? LEFT_PARENTHESIS WS? marker WS? RIGHT_PARENTHESIS WS?
| WS? marker_item WS?
"""
tokenizer.consume("WS")
if tokenizer.check("LEFT_PARENTHESIS", peek=True):
with tokenizer.enclosing_tokens(
"LEFT_PARENTHESIS",
"RIGHT_PARENTHESIS",
around="marker expression",
):
tokenizer.consume("WS")
marker: MarkerAtom = _parse_marker(tokenizer)
tokenizer.consume("WS")
else:
marker = _parse_marker_item(tokenizer)
tokenizer.consume("WS")
return marker
def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem:
"""
marker_item = WS? marker_var WS? marker_op WS? marker_var WS?
"""
tokenizer.consume("WS")
marker_var_left = _parse_marker_var(tokenizer)
tokenizer.consume("WS")
marker_op = _parse_marker_op(tokenizer)
tokenizer.consume("WS")
marker_var_right = _parse_marker_var(tokenizer)
tokenizer.consume("WS")
return (marker_var_left, marker_op, marker_var_right)
def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: # noqa: RET503
"""
marker_var = VARIABLE | QUOTED_STRING
"""
if tokenizer.check("VARIABLE"):
return process_env_var(tokenizer.read().text.replace(".", "_"))
elif tokenizer.check("QUOTED_STRING"):
token = tokenizer.read()
try:
return process_python_str(token.text)
except (SyntaxError, ValueError) as exc:
raise ParserSyntaxError(
"Invalid quoted string",
source=tokenizer.source,
span=(token.position, token.position + len(token.text)),
) from exc
else:
tokenizer.raise_syntax_error(
message="Expected a marker variable or quoted string"
)
def process_env_var(env_var: str) -> Variable:
if env_var in ("platform_python_implementation", "python_implementation"):
return Variable("platform_python_implementation")
else:
return Variable(env_var)
def process_python_str(python_str: str) -> Value:
value = ast.literal_eval(python_str)
return Value(str(value))
def _parse_marker_op(tokenizer: Tokenizer) -> Op:
"""
marker_op = IN | NOT IN | OP
"""
if tokenizer.check("IN"):
tokenizer.read()
return Op("in")
elif tokenizer.check("NOT"):
tokenizer.read()
tokenizer.expect("WS", expected="whitespace after 'not'")
tokenizer.expect("IN", expected="'in' after 'not'")
return Op("not in")
elif tokenizer.check("OP"):
return Op(tokenizer.read().text)
else:
return tokenizer.raise_syntax_error(
"Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in"
)
@@ -0,0 +1,836 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
"""Private version-range helpers used by :mod:`packaging.specifiers`."""
from __future__ import annotations
import enum
import functools
from typing import (
TYPE_CHECKING,
Any,
Final,
)
from .version import InvalidVersion, Version
if TYPE_CHECKING:
from collections.abc import Callable, Iterable, Iterator, Sequence
from typing import Union
# Total-order key for comparing two boundaries (boundary-vs-boundary only).
# The post slot may be ``_BOUNDARY_INF`` for an AFTER_POSTS boundary.
_BoundaryOrderSuffix = tuple[int, int, int, Union[int, float], int, int]
_BoundaryOrderKey = tuple[int, tuple[int, ...], _BoundaryOrderSuffix, float]
__all__ = [
"FULL_RANGE",
"bounds_for_spec",
"coerce_version",
"filter_by_ranges",
"intersect_ranges",
"intersect_specifier_bounds",
"least_version_above",
"matches_bounds_only",
"range_is_empty",
"ranges_are_prerelease_only",
"resolve_prereleases",
"standard_ranges",
"wildcard_ranges",
]
#: The smallest possible PEP 440 version. No valid version is less than this.
MIN_VERSION: Final[Version] = Version("0.dev0")
#: The smallest non-pre-release version, i.e. the nearest non-pre-release at or
#: above the ``-inf`` floor.
MIN_RELEASE: Final[Version] = Version("0")
#: Sorts above any real post number and any local label, so a boundary can be
#: ordered above the version family it covers when two boundaries are compared.
_BOUNDARY_INF: Final[float] = float("inf")
class BoundaryKind(enum.Enum):
"""Where a boundary marker sits in the version ordering."""
AFTER_LOCALS = enum.auto() # after V+local, before V.post0
AFTER_POSTS = enum.auto() # after V.postN, before next release
@functools.total_ordering
class BoundaryVersion:
"""A point on the version line between two real PEP 440 versions.
Relative to a base version V::
V < V+local < AFTER_LOCALS(V) < V.post0 < AFTER_POSTS(V)
AFTER_LOCALS is the upper bound of ``<=V``, ``==V``, ``!=V`` (no
local), and the lower bound of the upper-side range of ``!=V``.
AFTER_POSTS is the lower bound of ``>V`` (V final or pre-release),
excluding V's post-releases per PEP 440.
"""
__slots__ = (
"_cached_dev",
"_cached_epoch",
"_cached_post",
"_cached_pre",
"_cached_trimmed_release",
"kind",
"version",
)
def __init__(self, version: Version, kind: BoundaryKind) -> None:
self.version = version
self.kind = kind
self._cached_trimmed_release = trim_release(version.release)
self._cached_epoch = version.epoch
self._cached_pre = version.pre
self._cached_post = version.post
self._cached_dev = version.dev
def _is_family(self, other: Version) -> bool:
"""Is ``other`` a version that this boundary sorts above?"""
if other.epoch != self._cached_epoch:
return False
# Inline release-trim comparison: other.release matches the
# trimmed release iff its leading slice is equal and any extra
# components are zero. Avoids trim_release's tuple allocation.
other_release = other.release
trimmed_release = self._cached_trimmed_release
trimmed_length = len(trimmed_release)
if len(other_release) < trimmed_length:
return False
if other_release[:trimmed_length] != trimmed_release:
return False
for i in range(trimmed_length, len(other_release)):
if other_release[i] != 0:
return False
if other.pre != self._cached_pre:
return False
if self.kind == BoundaryKind.AFTER_LOCALS:
# Local family: same public version, any local label.
return other.post == self._cached_post and other.dev == self._cached_dev
# Post family: V itself + any post-release of V.
return other.dev == self._cached_dev or other.post is not None
def _order_key(self) -> _BoundaryOrderKey:
"""Sort key placing this boundary just above the versions it covers.
It extends ``V``'s comparison key ``(epoch, release, suffix)`` with
a trailing ``_BOUNDARY_INF`` local component, so the key sorts after
``V`` and every ``V+local`` (whose keys carry a real, finite local
segment). ``suffix`` is the 6-int comparison suffix
``(pre_rank, pre_n, post_rank, post_n, dev_rank, dev_n)``.
For an AFTER_POSTS boundary the suffix is replaced with one whose
post number is ``_BOUNDARY_INF``, so the key also sorts after every
``V.postN``. An AFTER_LOCALS boundary uses ``V``'s suffix unchanged.
"""
version_key = self.version._key
suffix: _BoundaryOrderSuffix = version_key[2]
if self.kind == BoundaryKind.AFTER_POSTS:
suffix = (suffix[0], suffix[1], 1, _BOUNDARY_INF, 1, 0)
return version_key[0], version_key[1], suffix, _BOUNDARY_INF
def __eq__(self, other: object) -> bool:
# Key off the order key so equality matches the ``<`` / ``>`` order:
# ``AFTER_POSTS(1.0)`` and ``AFTER_POSTS(1.0.post1)`` are the same point.
if isinstance(other, BoundaryVersion):
return self._order_key() == other._order_key()
return NotImplemented
def __lt__(self, other: BoundaryVersion | Version) -> bool:
if isinstance(other, BoundaryVersion):
return self._order_key() < other._order_key()
# boundary < other_version iff V < other AND other not in family.
# The cheap V >= other path short-circuits before the family check.
if not (self.version < other):
return False
return not self._is_family(other)
def __gt__(self, other: BoundaryVersion | Version) -> bool:
# Defined directly to bypass functools.total_ordering's
# NotImplemented round-trip on reflected ``Version < boundary``.
if isinstance(other, BoundaryVersion):
return self._order_key() > other._order_key()
if self.version >= other:
return True
return self._is_family(other)
def __hash__(self) -> int:
# Keyed to ``__eq__`` (the order key), so equal boundaries hash equal.
return hash(self._order_key())
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.version!r}, {self.kind.name})"
if TYPE_CHECKING:
_VersionOrBoundary = Union[Version, BoundaryVersion, None]
@functools.total_ordering
class LowerBound:
"""Lower bound of a version range.
A version *v* of ``None`` means unbounded below (-inf).
At equal versions, ``[v`` sorts before ``(v`` because an inclusive
bound starts earlier.
"""
__slots__ = ("_above", "inclusive", "version")
def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None:
self.version = version
self.inclusive = inclusive
# Pre-bind a predicate "is parsed at or above this lower
# bound?" for the hot filter / contains loops. One direct
# call per check, no operator-dispatch chain.
if version is None:
self._above: Callable[[Version], bool] | None = None
elif isinstance(version, BoundaryVersion):
# >V produces an AFTER_POSTS lower bound; the upper-side
# range of !=V produces an AFTER_LOCALS lower bound.
if version.kind == BoundaryKind.AFTER_POSTS:
self._above = _make_above_after_posts(version.version)
else:
self._above = _make_above_after_locals(version.version)
elif inclusive:
self._above = version.__le__
else:
self._above = version.__lt__
def __eq__(self, other: object) -> bool:
if not isinstance(other, LowerBound):
return NotImplemented
return self.version == other.version and self.inclusive == other.inclusive
def __lt__(self, other: LowerBound) -> bool:
if not isinstance(other, LowerBound):
return NotImplemented
# -inf < anything (except -inf itself).
if self.version is None:
return other.version is not None
if other.version is None:
return False
if self.version != other.version:
return self.version < other.version
# [v < (v: inclusive starts earlier.
return self.inclusive and not other.inclusive
def __hash__(self) -> int:
return hash((self.version, self.inclusive))
def __repr__(self) -> str:
bracket = "[" if self.inclusive else "("
return f"<{self.__class__.__name__} {bracket}{self.version!r}>"
@functools.total_ordering
class UpperBound:
"""Upper bound of a version range.
A version *v* of ``None`` means unbounded above (+inf).
At equal versions, ``v)`` sorts before ``v]`` because an exclusive
bound ends earlier.
"""
__slots__ = ("_below", "inclusive", "version")
def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None:
self.version = version
self.inclusive = inclusive
# Pre-bind a predicate "is parsed at or below this upper
# bound?". See LowerBound for the rationale.
if version is None:
self._below: Callable[[Version], bool] | None = None
elif isinstance(version, BoundaryVersion):
# Standard specifiers only ever produce AFTER_LOCALS upper
# bounds (from <=V / ==V / !=V with no local).
if version.kind == BoundaryKind.AFTER_LOCALS:
self._below = _make_below_after_locals(version.version)
else:
# An AFTER_POSTS upper is not produced by any specifier, but
# range algebra reaches it: complementing ``>V`` flips the
# ``AFTER_POSTS(V)`` lower into this upper bound.
self._below = version.__ge__
elif inclusive:
self._below = version.__ge__
else:
self._below = version.__gt__
def __eq__(self, other: object) -> bool:
if not isinstance(other, UpperBound):
return NotImplemented
return self.version == other.version and self.inclusive == other.inclusive
def __lt__(self, other: UpperBound) -> bool:
if not isinstance(other, UpperBound):
return NotImplemented
# Nothing < +inf (except +inf itself).
if self.version is None:
return False
if other.version is None:
return True
if self.version != other.version:
return self.version < other.version
# v) < v]: exclusive ends earlier.
return not self.inclusive and other.inclusive
def __hash__(self) -> int:
return hash((self.version, self.inclusive))
def __repr__(self) -> str:
bracket = "]" if self.inclusive else ")"
return f"<{self.__class__.__name__} {self.version!r}{bracket}>"
if TYPE_CHECKING:
#: A single contiguous interval as a (lower, upper) bound pair.
Interval = tuple[LowerBound, UpperBound]
NEG_INF: Final[LowerBound] = LowerBound(None, False)
POS_INF: Final[UpperBound] = UpperBound(None, False)
FULL_RANGE: Final[tuple[Interval]] = ((NEG_INF, POS_INF),)
def trim_release(release: tuple[int, ...]) -> tuple[int, ...]:
"""Strip trailing zeros from a release tuple for normalized comparison."""
end = len(release)
while end > 1 and release[end - 1] == 0:
end -= 1
return release if end == len(release) else release[:end]
def _next_prefix_dev0(version: Version) -> Version:
"""Smallest version in the next prefix: 1.2 -> 1.3.dev0."""
release = (*version.release[:-1], version.release[-1] + 1)
return Version.from_parts(epoch=version.epoch, release=release, dev=0)
def _base_dev0(version: Version) -> Version:
"""The .dev0 of a version's base release: 1.2 -> 1.2.dev0."""
return Version.from_parts(epoch=version.epoch, release=version.release, dev=0)
def coerce_version(version: Version | str) -> Version | None:
if not isinstance(version, Version):
try:
version = Version(version)
except InvalidVersion:
return None
return version
def _make_above_after_posts(version: Version) -> Callable[[Version], bool]:
"""Predicate ``parsed > AFTER_POSTS(V)`` for a lower bound.
Per PEP 440, ``>V`` excludes V's post-releases unless V is itself
a post-release. AFTER_POSTS sits above V and every V.postN (with
or without local), and just below the next release.
"""
version_ge = version.__ge__
version_epoch = version.epoch
version_pre = version.pre
version_release_trimmed = trim_release(version.release)
trimmed_length = len(version_release_trimmed)
def above(parsed: Version) -> bool:
if version_ge(parsed):
return False
# parsed > V cmpkey-wise: above the boundary iff NOT in V's
# post family.
if parsed.epoch != version_epoch:
return True
parsed_release = parsed.release
if len(parsed_release) < trimmed_length:
return True
if parsed_release[:trimmed_length] != version_release_trimmed:
return True
for i in range(trimmed_length, len(parsed_release)):
if parsed_release[i] != 0:
return True
if parsed.pre != version_pre:
return True
# Same release and pre as V: parsed is in V's post family (V itself,
# V+local, or V.postN), which the boundary sits above. A V.devN
# (different dev, no post) sorts before V and was already caught by
# ``version_ge`` above, so the answer here is always "not above".
return False
return above
def _make_above_after_locals(version: Version) -> Callable[[Version], bool]:
"""Predicate ``parsed > AFTER_LOCALS(V)`` for a lower bound.
Used by the upper-side range of ``!=V`` (when V has no local
segment). AFTER_LOCALS sits above V and every ``V+local`` but
just below ``V.post0``.
"""
version_ge = version.__ge__
version_epoch = version.epoch
version_pre = version.pre
version_post = version.post
version_dev = version.dev
version_release_trimmed = trim_release(version.release)
trimmed_length = len(version_release_trimmed)
def above(parsed: Version) -> bool:
if version_ge(parsed):
return False
# parsed > V cmpkey-wise: above the boundary iff NOT in V's
# local family (same public version, any local segment).
if parsed.epoch != version_epoch:
return True
parsed_release = parsed.release
if len(parsed_release) < trimmed_length:
return True
if parsed_release[:trimmed_length] != version_release_trimmed:
return True
for i in range(trimmed_length, len(parsed_release)):
if parsed_release[i] != 0:
return True
if parsed.pre != version_pre:
return True
if parsed.post != version_post:
return True
return parsed.dev != version_dev
return above
def _make_below_after_locals(version: Version) -> Callable[[Version], bool]:
"""Predicate ``parsed <= AFTER_LOCALS(V)`` for an upper bound.
Used by ``<=V``, ``==V``, ``!=V`` (no local). ``parsed`` is at or
below the boundary when it is at or below V cmpkey-wise, or when
it is in V's local family.
"""
version_ge = version.__ge__
version_epoch = version.epoch
version_pre = version.pre
version_post = version.post
version_dev = version.dev
version_release_trimmed = trim_release(version.release)
trimmed_length = len(version_release_trimmed)
def below(parsed: Version) -> bool:
if version_ge(parsed):
return True
# parsed > V cmpkey-wise: below the boundary iff in V's local
# family.
if parsed.epoch != version_epoch:
return False
parsed_release = parsed.release
if len(parsed_release) < trimmed_length:
return False
if parsed_release[:trimmed_length] != version_release_trimmed:
return False
for i in range(trimmed_length, len(parsed_release)):
if parsed_release[i] != 0:
return False
if parsed.pre != version_pre:
return False
if parsed.post != version_post:
return False
return parsed.dev == version_dev
return below
def least_version_above(boundary: BoundaryVersion) -> Version | None:
"""Smallest real version strictly above *boundary*, or ``None`` if none exists."""
base = boundary.version
if boundary.kind == BoundaryKind.AFTER_LOCALS:
# AFTER_LOCALS(V) sits just below V.post0, so its least successor is
# V.post0.dev0 (V.dev(N+1) if V has a dev, V.post(N+1).dev0 if a post).
if base.dev is not None:
return base.__replace__(dev=base.dev + 1, local=None)
next_post = (base.post + 1) if base.post is not None else 0
return base.__replace__(post=next_post, dev=0, local=None)
# AFTER_POSTS(V): a pre-release V steps to the next pre-release's .dev0;
# a final-release AFTER_POSTS has no least successor.
if base.pre is not None:
kind, number = base.pre
return base.__replace__(pre=(kind, number + 1), post=None, dev=0, local=None)
return None
def range_is_empty(lower: LowerBound, upper: UpperBound) -> bool:
"""True when the range defined by *lower* and *upper* contains no versions.
A boundary lower sits just below the next real version, so an ordered pair
is still empty when the upper excludes that least successor:
``(AFTER_POSTS(1.0a1), 1.0a2.dev0)`` holds no version.
"""
if upper.version is None:
return False
if lower.version is None:
# Nothing sorts below MIN_VERSION, so an exclusive upper at or below it
# leaves an empty floor interval such as ``(-inf, 0.dev0)``.
return (
not upper.inclusive
and isinstance(upper.version, Version)
and upper.version <= MIN_VERSION
)
if isinstance(lower.version, BoundaryVersion):
successor = least_version_above(lower.version)
if successor is not None:
if upper.version == successor:
return not upper.inclusive
return upper.version < successor
if lower.version == upper.version:
return not (lower.inclusive and upper.inclusive)
return lower.version > upper.version
def intersect_ranges(
left: Sequence[Interval],
right: Sequence[Interval],
) -> list[Interval]:
"""Intersect two sorted, non-overlapping range lists (two-pointer merge)."""
result: list[Interval] = []
left_index = right_index = 0
while left_index < len(left) and right_index < len(right):
left_lower, left_upper = left[left_index]
right_lower, right_upper = right[right_index]
lower = max(left_lower, right_lower)
upper = min(left_upper, right_upper)
if not range_is_empty(lower, upper):
result.append((lower, upper))
# Advance whichever side has the smaller upper bound.
if left_upper < right_upper:
left_index += 1
else:
right_index += 1
return result
def filter_by_ranges(
ranges: Sequence[Interval],
iterable: Iterable[Any],
key: Callable[[Any], Version | str] | None,
prereleases: bool | None,
region: Sequence[Interval] = (),
) -> Iterator[Any]:
"""Filter *iterable* against precomputed version *ranges*.
With ``prereleases=None``, the PEP 440 default applies: pre-releases are
excluded unless no final matches, in which case buffered pre-releases come
out at the end. A pre-release inside the opt-in ``region`` is the exception:
it is force-admitted in place, as ``prereleases=True`` would yield it. A
force-admitted pre-release is not a final, so it never suppresses the buffer.
"""
if prereleases is None:
prerelease_buffer: list[Any] = []
found_final = False
if len(ranges) == 1:
# Hot path: most specifiers and small SpecifierSets reduce to
# a single contiguous range.
lower, upper = ranges[0]
above = lower._above
below = upper._below
for item in iterable:
parsed = coerce_version(item if key is None else key(item))
if parsed is None:
continue
if above is not None and not above(parsed):
continue
if below is not None and not below(parsed):
continue
if not parsed.is_prerelease:
found_final = True
yield item
elif region and matches_bounds_only(region, parsed):
yield item
elif not found_final:
prerelease_buffer.append(item)
if not found_final:
yield from prerelease_buffer
return
for item in iterable:
parsed = coerce_version(item if key is None else key(item))
if parsed is None:
continue
for lower, upper in ranges:
above = lower._above
if above is not None and not above(parsed):
break
below = upper._below
if below is None or below(parsed):
if not parsed.is_prerelease:
found_final = True
yield item
elif region and matches_bounds_only(region, parsed):
yield item
elif not found_final:
prerelease_buffer.append(item)
break
if not found_final:
yield from prerelease_buffer
return
exclude_prereleases = prereleases is False
if len(ranges) == 1:
# Hot path: most specifiers and small SpecifierSets reduce to
# a single contiguous range.
lower, upper = ranges[0]
above = lower._above
below = upper._below
for item in iterable:
parsed = coerce_version(item if key is None else key(item))
if parsed is None:
continue
if exclude_prereleases and parsed.is_prerelease:
continue
if above is not None and not above(parsed):
continue
if below is None or below(parsed):
yield item
return
for item in iterable:
parsed = coerce_version(item if key is None else key(item))
if parsed is None:
continue
if exclude_prereleases and parsed.is_prerelease:
continue
for lower, upper in ranges:
above = lower._above
if above is not None and not above(parsed):
break
below = upper._below
if below is None or below(parsed):
yield item
break
def _nearest_release_above_prerelease(version: Version) -> Version:
"""Smallest non-pre-release at or above a pre-release *version*."""
if version.pre is not None:
# An a/b/rc pre-release drops to its final release, which outranks
# every post-release of that pre-release (1.0a1.post0 -> 1.0).
return version.__replace__(pre=None, post=None, dev=None, local=None)
# A dev-only release keeps its post-release (1.0.post0.dev0 -> 1.0.post0,
# whose final 1.0 sorts below it).
return version.__replace__(dev=None, local=None)
def _lowest_release_at_or_above(value: Version | BoundaryVersion | None) -> Version:
"""Smallest non-pre-release version at or above *value*.
``None`` is the ``-inf`` floor, whose nearest non-pre-release is
:data:`MIN_RELEASE`.
"""
if value is None:
return MIN_RELEASE
if isinstance(value, BoundaryVersion):
inner_version = value.version
if inner_version.is_prerelease:
return _nearest_release_above_prerelease(inner_version)
# AFTER_LOCALS(1.0) -> nearest non-pre is 1.0.post0
# AFTER_LOCALS(1.0.post0) -> nearest non-pre is 1.0.post1
next_post = (inner_version.post + 1) if inner_version.post is not None else 0
return inner_version.__replace__(post=next_post, local=None)
if not value.is_prerelease:
return value
return _nearest_release_above_prerelease(value)
def ranges_are_prerelease_only(ranges: Sequence[Interval]) -> bool:
"""True when every range in *ranges* contains only pre-releases.
Used to detect unsatisfiable specifier sets when ``prereleases=False``:
if every range is pre-release-only, every contained version is excluded.
"""
for lower, upper in ranges:
nearest = _lowest_release_at_or_above(lower.version)
if upper.version is None or nearest < upper.version:
return False
if nearest == upper.version and upper.inclusive:
return False
return True
def wildcard_ranges(op: str, base: Version) -> list[Interval]:
"""Ranges for ==V.* and !=V.*.
==1.2.* -> [1.2.dev0, 1.3.dev0); !=1.2.* -> complement.
"""
lower = _base_dev0(base)
upper = _next_prefix_dev0(base)
if op == "==":
return [(LowerBound(lower, True), UpperBound(upper, False))]
# !=
return [
(NEG_INF, UpperBound(lower, False)),
(LowerBound(upper, True), POS_INF),
]
def standard_ranges(op: str, version: Version, has_local: bool) -> list[Interval]:
"""Ranges for the standard PEP 440 operators (no wildcard, no ===).
*has_local* indicates whether the spec string included a ``+local``
segment; relevant only for ``==`` / ``!=`` to decide whether the
upper bound includes V's local family.
"""
if op == ">=":
return [(LowerBound(version, True), POS_INF)]
if op == "<=":
return [
(
NEG_INF,
UpperBound(BoundaryVersion(version, BoundaryKind.AFTER_LOCALS), True),
)
]
if op == ">":
if version.dev is not None:
# >V.devN: dev versions have no post-releases, so the
# next real version is V.dev(N+1).
lower_bound = version.__replace__(dev=version.dev + 1, local=None)
return [(LowerBound(lower_bound, True), POS_INF)]
if version.post is not None:
# >V.postN: next real version is V.post(N+1).dev0.
lower_bound = version.__replace__(post=version.post + 1, dev=0, local=None)
return [(LowerBound(lower_bound, True), POS_INF)]
# >V (final or pre-release V): exclude V itself, V+local, and
# every V.postN per PEP 440.
return [
(
LowerBound(BoundaryVersion(version, BoundaryKind.AFTER_POSTS), False),
POS_INF,
)
]
if op == "<":
# <V excludes pre-releases of V when V is not a pre-release.
# V.dev0 is the earliest pre-release of V.
bound = (
version if version.is_prerelease else version.__replace__(dev=0, local=None)
)
if bound <= MIN_VERSION:
return []
return [(NEG_INF, UpperBound(bound, False))]
# ==, !=: local versions of V match when the spec has no local segment.
after_locals = BoundaryVersion(version, BoundaryKind.AFTER_LOCALS)
upper = version if has_local else after_locals
if op == "==":
return [(LowerBound(version, True), UpperBound(upper, True))]
if op == "!=":
return [
(NEG_INF, UpperBound(version, False)),
(LowerBound(upper, False), POS_INF),
]
if op == "~=":
prefix = version.__replace__(release=version.release[:-1])
return [
(LowerBound(version, True), UpperBound(_next_prefix_dev0(prefix), False))
]
raise ValueError(f"Unknown operator: {op!r}") # pragma: no cover
def bounds_for_spec(op: str, version_str: str, version: Version) -> list[Interval]:
"""Ranges for one specifier's ``(op, version_str)``.
Dispatches between the wildcard and standard builders. ``version`` is the
parsed ``version_str`` (its base, without the trailing ``.*``, for
wildcards). ``===`` is not handled here; its match is a literal string
compared in :mod:`packaging.specifiers`.
"""
if version_str.endswith(".*"):
return wildcard_ranges(op, version)
return standard_ranges(op, version, "+" in version_str)
def intersect_specifier_bounds(
per_specifier_ranges: Iterable[Sequence[Interval]],
) -> Sequence[Interval]:
"""Intersect each specifier's ranges into a single sequence.
Short-circuits once the running intersection is empty, since no later
specifier can revive it. Callers must pass at least one specifier.
"""
result: Sequence[Interval] | None = None
for sub in per_specifier_ranges:
if result is None:
result = sub
else:
result = intersect_ranges(result, sub)
if not result:
break
if result is None: # pragma: no cover - callers guard non-empty input
raise RuntimeError("intersect_specifier_bounds called with no specifiers")
return result
def matches_bounds_only(ranges: Sequence[Interval], version: Version) -> bool:
"""Whether ``version`` falls within any of ``ranges``.
The pure bounds membership test, for a single already-parsed version with
no pre-release policy applied. ``ranges`` are sorted and non-overlapping,
so a version below one range's lower bound is below every later range too.
"""
for lower, upper in ranges:
above = lower._above
if above is not None and not above(version):
return False
below = upper._below
if below is None or below(version):
return True
return False
def resolve_prereleases(
configured: bool | None, autodetected: bool | None
) -> bool | None:
"""Resolve a specifier's effective default pre-release policy.
An explicit ``configured`` value wins; otherwise an autodetected ``True``
propagates and anything else falls back to the PEP 440 default (``None``).
"""
if configured is not None:
return configured
if autodetected:
return True
return None
@@ -0,0 +1,33 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
"""Backward-compatibility shim for unpickling Version objects serialized before
packaging 26.1.
Old pickles reference ``packaging._structures.InfinityType`` and
``packaging._structures.NegativeInfinityType``. This module provides minimal
stand-in classes so that ``pickle.loads()`` can resolve those references.
The deserialized objects are not used for comparisons — ``Version.__setstate__``
discards the stale ``_key`` cache and recomputes it from the core version fields.
"""
from __future__ import annotations
class InfinityType:
"""Stand-in for the removed ``InfinityType`` used in old comparison keys."""
def __repr__(self) -> str:
return "Infinity"
class NegativeInfinityType:
"""Stand-in for the removed ``NegativeInfinityType`` used in old comparison keys."""
def __repr__(self) -> str:
return "-Infinity"
Infinity = InfinityType()
NegativeInfinity = NegativeInfinityType()
@@ -0,0 +1,200 @@
from __future__ import annotations
import contextlib
import re
from dataclasses import dataclass
from typing import TYPE_CHECKING, NoReturn
from .specifiers import Specifier
if TYPE_CHECKING:
from collections.abc import Generator, Mapping
@dataclass
class Token:
__slots__ = ("name", "position", "text")
name: str
text: str
position: int
class ParserSyntaxError(Exception):
"""The provided source text could not be parsed correctly."""
def __init__(
self,
message: str,
*,
source: str,
span: tuple[int, int],
) -> None:
self.span = span
self.message = message
self.source = source
super().__init__()
def __str__(self) -> str:
marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^"
return f"{self.message}\n {self.source}\n {marker}"
DEFAULT_RULES: dict[str, re.Pattern[str]] = {
"LEFT_PARENTHESIS": re.compile(r"\("),
"RIGHT_PARENTHESIS": re.compile(r"\)"),
"LEFT_BRACKET": re.compile(r"\["),
"RIGHT_BRACKET": re.compile(r"\]"),
"SEMICOLON": re.compile(r";"),
"COMMA": re.compile(r","),
"QUOTED_STRING": re.compile(
r"""
(
('[^']*')
|
("[^"]*")
)
""",
re.VERBOSE,
),
"OP": re.compile(r"(===|==|~=|!=|<=|>=|<|>)"),
"BOOLOP": re.compile(r"\b(or|and)\b"),
"IN": re.compile(r"\bin\b"),
"NOT": re.compile(r"\bnot\b"),
"VARIABLE": re.compile(
r"""
\b(
python_version
|python_full_version
|os[._]name
|sys[._]platform
|platform_(release|system)
|platform[._](version|machine|python_implementation)
|python_implementation
|implementation_(name|version)
|extras?
|dependency_groups
)\b
""",
re.VERBOSE,
),
"SPECIFIER": re.compile(
Specifier._specifier_regex_str,
re.VERBOSE | re.IGNORECASE,
),
"AT": re.compile(r"\@"),
"URL": re.compile(r"[^ \t]+"),
"IDENTIFIER": re.compile(r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b"),
"VERSION_PREFIX_TRAIL": re.compile(r"\.\*"),
"VERSION_LOCAL_LABEL_TRAIL": re.compile(r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*"),
"WS": re.compile(r"[ \t]+"),
"END": re.compile(r"\Z"),
}
class Tokenizer:
"""Context-sensitive token parsing.
Provides methods to examine the input stream to check whether the next token
matches.
"""
__slots__ = ("next_token", "position", "rules", "source")
def __init__(
self,
source: str,
*,
rules: Mapping[str, re.Pattern[str]],
) -> None:
self.source = source
self.rules = rules
self.next_token: Token | None = None
self.position = 0
def consume(self, name: str) -> None:
"""Move beyond provided token name, if at current position."""
if self.check(name):
self.read()
def check(self, name: str, *, peek: bool = False) -> bool:
"""Check whether the next token has the provided name.
By default, if the check succeeds, the token *must* be read before
another check. If `peek` is set to `True`, the token is not loaded and
would need to be checked again.
"""
assert self.next_token is None, (
f"Cannot check for {name!r}, already have {self.next_token!r}"
)
assert name in self.rules, f"Unknown token name: {name!r}"
expression = self.rules[name]
match = expression.match(self.source, self.position)
if match is None:
return False
if not peek:
self.next_token = Token(name, match[0], self.position)
return True
def expect(self, name: str, *, expected: str) -> Token:
"""Expect a certain token name next, failing with a syntax error otherwise.
The token is read and returned.
"""
if not self.check(name):
raise self.raise_syntax_error(f"Expected {expected}")
return self.read()
def read(self) -> Token:
"""Consume the next token and return it."""
token = self.next_token
assert token is not None
self.position += len(token.text)
self.next_token = None
return token
def raise_syntax_error(
self,
message: str,
*,
span_start: int | None = None,
span_end: int | None = None,
) -> NoReturn:
"""Raise ParserSyntaxError at the given position."""
span = (
self.position if span_start is None else span_start,
self.position if span_end is None else span_end,
)
raise ParserSyntaxError(
message,
source=self.source,
span=span,
)
@contextlib.contextmanager
def enclosing_tokens(
self, open_token: str, close_token: str, *, around: str
) -> Generator[None, None, None]:
if self.check(open_token):
open_position = self.position
self.read()
else:
open_position = None
yield
if open_position is None:
return
if not self.check(close_token):
self.raise_syntax_error(
f"Expected matching {close_token} for {open_token}, after {around}",
span_start=open_position,
)
self.read()
@@ -0,0 +1,335 @@
from __future__ import annotations
import re
from collections.abc import Mapping, Sequence
from .errors import _ErrorCollector
from .requirements import InvalidRequirement, Requirement
__all__ = [
"CyclicDependencyGroup",
"DependencyGroupInclude",
"DependencyGroupResolver",
"DuplicateGroupNames",
"InvalidDependencyGroupObject",
"resolve_dependency_groups",
]
def __dir__() -> list[str]:
return __all__
# -----------
# Error Types
# -----------
class DuplicateGroupNames(ValueError):
"""
The same dependency groups were defined twice, with different non-normalized names.
.. versionadded:: 26.1
"""
class CyclicDependencyGroup(ValueError):
"""
The dependency group includes form a cycle.
.. versionadded:: 26.1
"""
def __init__(self, requested_group: str, group: str, include_group: str) -> None:
self.requested_group = requested_group
self.group = group
self.include_group = include_group
if include_group == group:
reason = f"{group} includes itself"
else:
reason = f"{include_group} -> {group}, {group} -> {include_group}"
super().__init__(
"Cyclic dependency group include while resolving "
f"{requested_group}: {reason}"
)
# Support pickling; ``args`` does not match ``__init__``'s signature.
def __reduce__(self) -> tuple[type[CyclicDependencyGroup], tuple[str, str, str]]:
return (self.__class__, (self.requested_group, self.group, self.include_group))
# in the PEP 735 spec, the tables in dependency group lists were described as
# "Dependency Object Specifiers", but the only defined type of object was a
# "Dependency Group Include" -- hence the naming of this error as "Object"
class InvalidDependencyGroupObject(ValueError):
"""
A member of a dependency group was identified as a dict, but was not in a valid
format.
.. versionadded:: 26.1
"""
# ------------------------
# Object Model & Interface
# ------------------------
class DependencyGroupInclude:
"""
A reference to another dependency group by name.
.. versionadded:: 26.1
"""
__slots__ = ("include_group",)
def __init__(self, include_group: str) -> None:
"""
Initialize a DependencyGroupInclude.
:param include_group: The name of the group referred to by this include.
"""
self.include_group = include_group
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.include_group!r})"
class DependencyGroupResolver:
"""
A resolver for Dependency Group data.
This class handles caching, name normalization, cycle detection, and other
parsing requirements. There are only two public methods for exploring the data:
``lookup()`` and ``resolve()``.
:param dependency_groups: A mapping, as provided via pyproject
``[dependency-groups]``.
.. versionadded:: 26.1
"""
def __init__(
self,
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],
) -> None:
errors = _ErrorCollector()
self.dependency_groups = _normalize_group_names(dependency_groups, errors)
# a map of group names to parsed data
self._parsed_groups: dict[
str, tuple[Requirement | DependencyGroupInclude, ...]
] = {}
# a map of group names to their ancestors, used for cycle detection
self._include_graph_ancestors: dict[str, tuple[str, ...]] = {}
# a cache of completed resolutions to Requirement lists
self._resolve_cache: dict[str, tuple[Requirement, ...]] = {}
errors.finalize("[dependency-groups] data was invalid")
def lookup(self, group: str) -> tuple[Requirement | DependencyGroupInclude, ...]:
"""
Lookup a group name, returning the parsed dependency data for that group.
This will not resolve includes.
:param group: the name of the group to lookup
"""
group = _normalize_name(group)
with _ErrorCollector().on_exit(
f"[dependency-groups] data for {group!r} was malformed"
) as errors:
return self._parse_group(group, errors)
def resolve(self, group: str) -> tuple[Requirement, ...]:
"""
Resolve a dependency group to a list of requirements.
:param group: the name of the group to resolve
"""
group = _normalize_name(group)
with _ErrorCollector().on_exit(
f"[dependency-groups] data for {group!r} was malformed"
) as errors:
return self._resolve(group, group, errors)
def _resolve(
self, group: str, requested_group: str, errors: _ErrorCollector
) -> tuple[Requirement, ...]:
"""
This is a helper for cached resolution to strings. It preserves the name of the
group which the user initially requested in order to present a clearer error in
the event that a cycle is detected.
:param group: The normalized name of the group to resolve.
:param requested_group: The group which was used in the original, user-facing
request.
"""
if group in self._resolve_cache:
return self._resolve_cache[group]
parsed = self._parse_group(group, errors)
resolved_group = []
for item in parsed:
if isinstance(item, Requirement):
resolved_group.append(item)
elif isinstance(item, DependencyGroupInclude):
include_group = _normalize_name(item.include_group)
# if a group is cyclic, record the error
# otherwise, follow the include_group reference
#
# this allows us to examine all includes in a group, even in the
# presence of errors
if include_group in self._include_graph_ancestors.get(group, ()):
errors.error(
CyclicDependencyGroup(
requested_group, group, item.include_group
)
)
else:
self._include_graph_ancestors[include_group] = (
*self._include_graph_ancestors.get(group, ()),
group,
)
resolved_group.extend(
self._resolve(include_group, requested_group, errors)
)
else: # pragma: no cover
raise NotImplementedError(
f"Invalid dependency group item after parse: {item}"
)
# in the event that errors were detected, present the group as empty and do not
# cache the result
# this ensures that repeated access to a cyclic group will raise multiple errors
if errors.errors:
return ()
self._resolve_cache[group] = tuple(resolved_group)
return self._resolve_cache[group]
def _parse_group(
self, group: str, errors: _ErrorCollector
) -> tuple[Requirement | DependencyGroupInclude, ...]:
# short circuit -- never do the work twice
if group in self._parsed_groups:
return self._parsed_groups[group]
if group not in self.dependency_groups:
errors.error(LookupError(f"Dependency group '{group}' not found"))
return ()
raw_group = self.dependency_groups[group]
if isinstance(raw_group, str):
errors.error(
TypeError(
f"Dependency group {group!r} contained a string rather than a list."
)
)
return ()
if not isinstance(raw_group, Sequence):
errors.error(
TypeError(f"Dependency group {group!r} is not a sequence type.")
)
return ()
elements: list[Requirement | DependencyGroupInclude] = []
for item in raw_group:
if isinstance(item, str):
# packaging.requirements.Requirement parsing ensures that this is a
# valid PEP 508 Dependency Specifier. Collect InvalidRequirement
# if it throws that.
with errors.collect(InvalidRequirement):
elements.append(Requirement(item))
elif isinstance(item, Mapping):
if tuple(item.keys()) != ("include-group",):
errors.error(
InvalidDependencyGroupObject(
f"Invalid dependency group item: {item!r}"
)
)
else:
include_group = item["include-group"]
if not isinstance(include_group, str):
msg = (
"Dependency group include-group value is not a string: "
f"{item!r}"
)
errors.error(TypeError(msg))
else:
elements.append(
DependencyGroupInclude(include_group=include_group)
)
else:
errors.error(TypeError(f"Invalid dependency group item: {item!r}"))
if errors.errors:
return ()
self._parsed_groups[group] = tuple(elements)
return self._parsed_groups[group]
# --------------------
# Functional Interface
# --------------------
def resolve_dependency_groups(
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]], /, *groups: str
) -> tuple[str, ...]:
"""
Resolve a dependency group to a tuple of requirements, as strings.
:param dependency_groups: the parsed contents of the ``[dependency-groups]`` table
from ``pyproject.toml``
:param groups: the name of the group(s) to resolve
.. versionadded:: 26.1
"""
resolver = DependencyGroupResolver(dependency_groups)
return tuple(str(r) for group in groups for r in resolver.resolve(group))
# ----------------
# internal helpers
# ----------------
_NORMALIZE_PATTERN = re.compile(r"[-_.]+")
def _normalize_name(name: str) -> str:
return _NORMALIZE_PATTERN.sub("-", name).lower()
def _normalize_group_names(
dependency_groups: Mapping[str, Sequence[str | Mapping[str, str]]],
errors: _ErrorCollector,
) -> dict[str, Sequence[str | Mapping[str, str]]]:
original_names: dict[str, list[str]] = {}
normalized_groups: dict[str, Sequence[str | Mapping[str, str]]] = {}
for group_name, value in dependency_groups.items():
normed_group_name = _normalize_name(group_name)
original_names.setdefault(normed_group_name, []).append(group_name)
normalized_groups[normed_group_name] = value
for normed_name, names in original_names.items():
if len(names) > 1:
errors.error(
DuplicateGroupNames(
"Duplicate dependency group names: "
f"{normed_name} ({', '.join(names)})"
)
)
return normalized_groups
@@ -0,0 +1,349 @@
from __future__ import annotations
import dataclasses
import re
import urllib.parse
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, Protocol, TypeVar
if TYPE_CHECKING: # pragma: no cover
import sys
from collections.abc import Collection
from urllib.parse import SplitResult
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self
__all__ = [
"ArchiveInfo",
"DirInfo",
"DirectUrl",
"DirectUrlValidationError",
"VcsInfo",
]
def __dir__() -> list[str]:
return __all__
_T = TypeVar("_T")
class _FromMappingProtocol(Protocol): # pragma: no cover
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
_FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol)
def _json_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
return {key: value for key, value in data if value is not None}
def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None:
"""Get a value from the dictionary and verify it's the expected type."""
if (value := d.get(key)) is None:
return None
if not isinstance(value, expected_type):
raise DirectUrlValidationError(
f"Unexpected type {type(value).__name__} "
f"(expected {expected_type.__name__})",
context=key,
)
return value
def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T:
"""Get a required value from the dictionary and verify it's the expected type."""
if (value := _get(d, expected_type, key)) is None:
raise _DirectUrlRequiredKeyError(key)
return value
def _get_object(
d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str
) -> _FromMappingProtocolT | None:
"""Get a dictionary value from the dictionary and convert it to a dataclass."""
if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract]
return None
try:
return target_type._from_dict(value)
except Exception as e:
raise DirectUrlValidationError(e, context=key) from e
_PEP610_USER_PASS_ENV_VARS_REGEX = re.compile(
r"^\$\{[A-Za-z0-9-_]+\}(:\$\{[A-Za-z0-9-_]+\})?$"
)
def _strip_auth_from_netloc(netloc: str, safe_user_passwords: Collection[str]) -> str:
if "@" not in netloc:
return netloc
user_pass, netloc_no_user_pass = netloc.rsplit("@", 1)
if user_pass in safe_user_passwords:
return netloc
if _PEP610_USER_PASS_ENV_VARS_REGEX.match(user_pass):
return netloc
return netloc_no_user_pass
def _strip_url(url: str, safe_user_passwords: Collection[str]) -> str:
"""url with user:password part removed unless it is formed with
environment variables as specified in PEP 610, or it is a safe user:password
such as `git`.
"""
parsed_url = urllib.parse.urlsplit(url)
netloc = _strip_auth_from_netloc(parsed_url.netloc, safe_user_passwords)
return urllib.parse.urlunsplit(
(
parsed_url.scheme,
netloc,
parsed_url.path,
parsed_url.query,
parsed_url.fragment,
)
)
def _file_url_has_absolute_path(parsed_url: SplitResult) -> bool:
return parsed_url.path.startswith("/")
class DirectUrlValidationError(Exception):
"""Raised when when input data is not spec-compliant.
.. versionadded:: 26.1
"""
context: str | None = None
message: str
def __init__(
self,
cause: str | Exception,
*,
context: str | None = None,
) -> None:
if isinstance(cause, DirectUrlValidationError):
if cause.context:
self.context = (
f"{context}.{cause.context}" if context else cause.context
)
else:
self.context = context # pragma: no cover
self.message = cause.message
else:
self.context = context
self.message = str(cause)
def __str__(self) -> str:
if self.context:
return f"{self.message} in {self.context!r}"
return self.message
class _DirectUrlRequiredKeyError(DirectUrlValidationError):
def __init__(self, key: str) -> None:
super().__init__("Missing required value", context=key)
@dataclasses.dataclass(frozen=True, init=False)
class VcsInfo:
"""The version control information of a :class:`DirectUrl`."""
vcs: str
commit_id: str
requested_revision: str | None = None
def __init__(
self,
*,
vcs: str,
commit_id: str,
requested_revision: str | None = None,
) -> None:
object.__setattr__(self, "vcs", vcs)
object.__setattr__(self, "commit_id", commit_id)
object.__setattr__(self, "requested_revision", requested_revision)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
# We can't validate vcs value because is not closed.
return cls(
vcs=_get_required(d, str, "vcs"),
requested_revision=_get(d, str, "requested_revision"),
commit_id=_get_required(d, str, "commit_id"),
)
@dataclasses.dataclass(frozen=True, init=False)
class ArchiveInfo:
"""The archive information of a :class:`DirectUrl`."""
hashes: Mapping[str, str] | None = None
def __init__(
self,
*,
hashes: Mapping[str, str] | None = None,
) -> None:
object.__setattr__(self, "hashes", hashes)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
hashes = _get(d, Mapping, "hashes") # type: ignore[type-abstract]
if hashes is not None and not all(isinstance(h, str) for h in hashes.values()):
raise DirectUrlValidationError(
"Hash values must be strings", context="hashes"
)
legacy_hash = _get(d, str, "hash")
if legacy_hash is not None:
if "=" not in legacy_hash:
raise DirectUrlValidationError(
"Invalid hash format (expected '<algorithm>=<hash>')",
context="hash",
)
hash_algorithm, hash_value = legacy_hash.split("=", 1)
if hashes is None:
# if `hashes` are not present, we can derive it from the legacy `hash`
hashes = {hash_algorithm: hash_value}
else:
# if `hashes` are present, the legacy `hash` must match one of them
if hash_algorithm not in hashes:
raise DirectUrlValidationError(
f"Algorithm {hash_algorithm!r} used in hash field "
f"is not present in hashes field",
context="hashes",
)
if hashes[hash_algorithm] != hash_value:
raise DirectUrlValidationError(
f"Algorithm {hash_algorithm!r} used in hash field "
f"has different value in hashes field",
context="hash",
)
return cls(hashes=hashes)
@dataclasses.dataclass(frozen=True, init=False)
class DirInfo:
"""The local directory information of a :class:`DirectUrl`."""
editable: bool | None = None
def __init__(
self,
*,
editable: bool | None = None,
) -> None:
object.__setattr__(self, "editable", editable)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
return cls(
editable=_get(d, bool, "editable"),
)
@dataclasses.dataclass(frozen=True, init=False)
class DirectUrl:
"""A class representing a direct URL.
.. versionadded:: 26.1
"""
url: str
archive_info: ArchiveInfo | None = None
vcs_info: VcsInfo | None = None
dir_info: DirInfo | None = None
subdirectory: str | None = None # XXX Path or str?
def __init__(
self,
*,
url: str,
archive_info: ArchiveInfo | None = None,
vcs_info: VcsInfo | None = None,
dir_info: DirInfo | None = None,
subdirectory: str | None = None,
) -> None:
object.__setattr__(self, "url", url)
object.__setattr__(self, "archive_info", archive_info)
object.__setattr__(self, "vcs_info", vcs_info)
object.__setattr__(self, "dir_info", dir_info)
object.__setattr__(self, "subdirectory", subdirectory)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
direct_url = cls(
url=_get_required(d, str, "url"),
archive_info=_get_object(d, ArchiveInfo, "archive_info"),
vcs_info=_get_object(d, VcsInfo, "vcs_info"),
dir_info=_get_object(d, DirInfo, "dir_info"),
subdirectory=_get(d, str, "subdirectory"),
)
if (
bool(direct_url.vcs_info)
+ bool(direct_url.archive_info)
+ bool(direct_url.dir_info)
) != 1:
raise DirectUrlValidationError(
"Exactly one of vcs_info, archive_info, dir_info must be present"
)
if direct_url.dir_info is not None:
parsed_url = urllib.parse.urlsplit(direct_url.url)
if parsed_url.scheme != "file":
raise DirectUrlValidationError(
"URL scheme must be file:// when dir_info is present",
context="url",
)
if not _file_url_has_absolute_path(parsed_url):
raise DirectUrlValidationError(
"File URL must be absolute when dir_info is present",
context="url",
)
# XXX subdirectory must be relative, can we, should we validate that here?
return direct_url
@classmethod
def from_dict(cls, d: Mapping[str, Any], /) -> Self:
"""Create and validate a DirectUrl instance from a JSON dictionary."""
return cls._from_dict(d)
def to_dict(
self,
*,
generate_legacy_hash: bool = False,
strip_user_password: bool = True,
safe_user_passwords: Collection[str] = ("git",),
) -> Mapping[str, Any]:
"""Convert the DirectUrl instance to a JSON dictionary.
:param generate_legacy_hash: If True, include a legacy `hash` field in
`archive_info` for backward compatibility with tools that don't
support the `hashes` field.
:param strip_user_password: If True, strip user:password from the URL
unless it is formed with environment variables as specified in PEP
610, or it is a safe user:password such as `git`.
:param safe_user_passwords: A collection of user:password strings that
should not be stripped from the URL even if `strip_user_password` is
True.
"""
res = dataclasses.asdict(self, dict_factory=_json_dict_factory)
if generate_legacy_hash and self.archive_info and self.archive_info.hashes:
hash_algorithm, hash_value = next(iter(self.archive_info.hashes.items()))
res["archive_info"]["hash"] = f"{hash_algorithm}={hash_value}"
if strip_user_password:
res["url"] = _strip_url(self.url, safe_user_passwords)
return res
def validate(self) -> None:
"""Validate the DirectUrl instance against the specification.
Raises :class:`DirectUrlValidationError` if invalid.
"""
self.from_dict(self.to_dict())
@@ -0,0 +1,94 @@
from __future__ import annotations
import contextlib
import dataclasses
import sys
import typing
__all__ = ["ExceptionGroup"]
def __dir__() -> list[str]:
return __all__
if sys.version_info >= (3, 11): # pragma: no cover
from builtins import ExceptionGroup
else: # pragma: no cover
class ExceptionGroup(Exception):
"""A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
If :external:exc:`ExceptionGroup` is already defined by Python itself,
that version is used instead.
"""
message: str
exceptions: list[Exception]
def __init__(self, message: str, exceptions: list[Exception]) -> None:
self.message = message
self.exceptions = exceptions
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})"
@dataclasses.dataclass
class _ErrorCollector:
"""
Collect errors into ExceptionGroups.
Used like this:
collector = _ErrorCollector()
# Add a single exception
collector.error(ValueError("one"))
# Supports nesting, including combining ExceptionGroups
with collector.collect():
raise ValueError("two")
collector.finalize("Found some errors")
Since making a collector and then calling finalize later is a common pattern,
a convenience method ``on_exit`` is provided.
"""
errors: list[Exception] = dataclasses.field(default_factory=list, init=False)
def finalize(self, msg: str) -> None:
"""Raise a group exception if there are any errors."""
if self.errors:
raise ExceptionGroup(msg, self.errors)
@contextlib.contextmanager
def on_exit(self, msg: str) -> typing.Generator[_ErrorCollector, None, None]:
"""
Calls finalize if no uncollected errors were present.
Uncollected errors are raised normally.
"""
yield self
self.finalize(msg)
@contextlib.contextmanager
def collect(self, *err_cls: type[Exception]) -> typing.Generator[None, None, None]:
"""
Context manager to collect errors into the error list.
Must be inside loops, as only one error can be collected at a time.
"""
error_classes = err_cls or (Exception,)
try:
yield
except ExceptionGroup as error:
self.errors.extend(error.exceptions)
except error_classes as error:
self.errors.append(error)
def error(
self,
error: Exception,
) -> None:
"""Add an error to the list."""
self.errors.append(error)
@@ -0,0 +1,198 @@
#######################################################################################
#
# Adapted from:
# https://github.com/pypa/hatch/blob/5352e44/backend/src/hatchling/licenses/parse.py
#
# MIT License
#
# Copyright (c) 2017-present Ofek Lev <oss@ofek.dev>
#
# 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.
#
#
# With additional allowance of arbitrary `LicenseRef-` identifiers, not just
# `LicenseRef-Public-Domain` and `LicenseRef-Proprietary`.
#
#######################################################################################
from __future__ import annotations
import re
from typing import NewType, cast
from ._spdx import EXCEPTIONS, LICENSES
__all__ = [
"InvalidLicenseExpression",
"NormalizedLicenseExpression",
"canonicalize_license_expression",
]
# Simple __dir__ implementation since there are no public submodules
def __dir__() -> list[str]:
return __all__
license_ref_allowed = re.compile("^[A-Za-z0-9.-]+$")
NormalizedLicenseExpression = NewType("NormalizedLicenseExpression", str)
"""
A :class:`typing.NewType` of :class:`str`, representing a normalized
License-Expression.
"""
class InvalidLicenseExpression(ValueError):
"""Raised when a license-expression string is invalid
>>> from packaging.licenses import canonicalize_license_expression
>>> canonicalize_license_expression("invalid")
Traceback (most recent call last):
...
packaging.licenses.InvalidLicenseExpression: Unknown license: 'invalid'
"""
def canonicalize_license_expression(
raw_license_expression: str,
) -> NormalizedLicenseExpression:
"""
This function takes a valid License-Expression, and returns the normalized
form of it.
The return type is typed as :class:`NormalizedLicenseExpression`. This
allows type checkers to help require that a string has passed through this
function before use.
:param str raw_license_expression: The License-Expression to canonicalize.
:raises InvalidLicenseExpression: If the License-Expression is invalid due to an
invalid/unknown license identifier or invalid syntax.
.. doctest::
>>> from packaging.licenses import canonicalize_license_expression
>>> canonicalize_license_expression("mit")
'MIT'
>>> canonicalize_license_expression("mit and (apache-2.0 or bsd-2-clause)")
'MIT AND (Apache-2.0 OR BSD-2-Clause)'
>>> canonicalize_license_expression("(mit")
Traceback (most recent call last):
...
InvalidLicenseExpression: Invalid license expression: '(mit'
>>> canonicalize_license_expression("Use-it-after-midnight")
Traceback (most recent call last):
...
InvalidLicenseExpression: Unknown license: 'Use-it-after-midnight'
"""
if not raw_license_expression:
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message)
# Pad any parentheses so tokenization can be achieved by merely splitting on
# whitespace.
license_expression = raw_license_expression.replace("(", " ( ").replace(")", " ) ")
licenseref_prefix = "LicenseRef-"
license_refs = {
ref.lower(): "LicenseRef-" + ref[len(licenseref_prefix) :]
for ref in license_expression.split()
if ref.lower().startswith(licenseref_prefix.lower())
}
# Normalize to lower case so we can look up licenses/exceptions
# and so boolean operators are Python-compatible.
license_expression = license_expression.lower()
tokens = license_expression.split()
# Rather than implementing a parenthesis/boolean logic parser, create an
# expression that Python can parse. Everything that is not involved with the
# grammar itself is replaced with the placeholder `False` and the resultant
# expression should become a valid Python expression.
python_tokens = []
for token in tokens:
if token not in {"or", "and", "with", "(", ")"}:
python_tokens.append("False")
elif token == "with":
python_tokens.append("or")
elif (
token == "("
and python_tokens
and python_tokens[-1] not in {"or", "and", "("}
) or (token == ")" and python_tokens and python_tokens[-1] == "("):
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message)
else:
python_tokens.append(token)
python_expression = " ".join(python_tokens)
try:
compile(python_expression, "", "eval")
except SyntaxError:
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message) from None
# Take a final pass to check for unknown licenses/exceptions.
normalized_tokens = []
last_license_start = False
for index, token in enumerate(tokens):
if token in {"or", "and", "with", "(", ")"}:
if token == "with" and (
not last_license_start
or index + 1 == len(tokens)
or tokens[index + 1] in {"or", "and", "with", "(", ")"}
):
message = f"Invalid license expression: {raw_license_expression!r}"
raise InvalidLicenseExpression(message)
normalized_tokens.append(token.upper())
last_license_start = False
continue
if normalized_tokens and normalized_tokens[-1] == "WITH":
if token not in EXCEPTIONS:
message = f"Unknown license exception: {token!r}"
raise InvalidLicenseExpression(message)
normalized_tokens.append(EXCEPTIONS[token]["id"])
last_license_start = False
else:
if token.endswith("+"):
final_token = token[:-1]
suffix = "+"
else:
final_token = token
suffix = ""
if final_token.startswith("licenseref-"):
license_ref_id = final_token[len("licenseref-") :]
if suffix or not license_ref_allowed.match(license_ref_id):
message = f"Invalid licenseref: {token!r}"
raise InvalidLicenseExpression(message)
normalized_tokens.append(license_refs[final_token])
else:
if final_token not in LICENSES:
message = f"Unknown license: {final_token!r}"
raise InvalidLicenseExpression(message)
normalized_tokens.append(LICENSES[final_token]["id"] + suffix)
last_license_start = True
normalized_expression = " ".join(normalized_tokens)
return cast(
"NormalizedLicenseExpression",
normalized_expression.replace("( ", "(").replace(" )", ")"),
)
@@ -0,0 +1,799 @@
from __future__ import annotations
from typing import TypedDict
class SPDXLicense(TypedDict):
id: str
deprecated: bool
class SPDXException(TypedDict):
id: str
deprecated: bool
VERSION = '3.27.0'
LICENSES: dict[str, SPDXLicense] = {
'0bsd': {'id': '0BSD', 'deprecated': False},
'3d-slicer-1.0': {'id': '3D-Slicer-1.0', 'deprecated': False},
'aal': {'id': 'AAL', 'deprecated': False},
'abstyles': {'id': 'Abstyles', 'deprecated': False},
'adacore-doc': {'id': 'AdaCore-doc', 'deprecated': False},
'adobe-2006': {'id': 'Adobe-2006', 'deprecated': False},
'adobe-display-postscript': {'id': 'Adobe-Display-PostScript', 'deprecated': False},
'adobe-glyph': {'id': 'Adobe-Glyph', 'deprecated': False},
'adobe-utopia': {'id': 'Adobe-Utopia', 'deprecated': False},
'adsl': {'id': 'ADSL', 'deprecated': False},
'afl-1.1': {'id': 'AFL-1.1', 'deprecated': False},
'afl-1.2': {'id': 'AFL-1.2', 'deprecated': False},
'afl-2.0': {'id': 'AFL-2.0', 'deprecated': False},
'afl-2.1': {'id': 'AFL-2.1', 'deprecated': False},
'afl-3.0': {'id': 'AFL-3.0', 'deprecated': False},
'afmparse': {'id': 'Afmparse', 'deprecated': False},
'agpl-1.0': {'id': 'AGPL-1.0', 'deprecated': True},
'agpl-1.0-only': {'id': 'AGPL-1.0-only', 'deprecated': False},
'agpl-1.0-or-later': {'id': 'AGPL-1.0-or-later', 'deprecated': False},
'agpl-3.0': {'id': 'AGPL-3.0', 'deprecated': True},
'agpl-3.0-only': {'id': 'AGPL-3.0-only', 'deprecated': False},
'agpl-3.0-or-later': {'id': 'AGPL-3.0-or-later', 'deprecated': False},
'aladdin': {'id': 'Aladdin', 'deprecated': False},
'amd-newlib': {'id': 'AMD-newlib', 'deprecated': False},
'amdplpa': {'id': 'AMDPLPA', 'deprecated': False},
'aml': {'id': 'AML', 'deprecated': False},
'aml-glslang': {'id': 'AML-glslang', 'deprecated': False},
'ampas': {'id': 'AMPAS', 'deprecated': False},
'antlr-pd': {'id': 'ANTLR-PD', 'deprecated': False},
'antlr-pd-fallback': {'id': 'ANTLR-PD-fallback', 'deprecated': False},
'any-osi': {'id': 'any-OSI', 'deprecated': False},
'any-osi-perl-modules': {'id': 'any-OSI-perl-modules', 'deprecated': False},
'apache-1.0': {'id': 'Apache-1.0', 'deprecated': False},
'apache-1.1': {'id': 'Apache-1.1', 'deprecated': False},
'apache-2.0': {'id': 'Apache-2.0', 'deprecated': False},
'apafml': {'id': 'APAFML', 'deprecated': False},
'apl-1.0': {'id': 'APL-1.0', 'deprecated': False},
'app-s2p': {'id': 'App-s2p', 'deprecated': False},
'apsl-1.0': {'id': 'APSL-1.0', 'deprecated': False},
'apsl-1.1': {'id': 'APSL-1.1', 'deprecated': False},
'apsl-1.2': {'id': 'APSL-1.2', 'deprecated': False},
'apsl-2.0': {'id': 'APSL-2.0', 'deprecated': False},
'arphic-1999': {'id': 'Arphic-1999', 'deprecated': False},
'artistic-1.0': {'id': 'Artistic-1.0', 'deprecated': False},
'artistic-1.0-cl8': {'id': 'Artistic-1.0-cl8', 'deprecated': False},
'artistic-1.0-perl': {'id': 'Artistic-1.0-Perl', 'deprecated': False},
'artistic-2.0': {'id': 'Artistic-2.0', 'deprecated': False},
'artistic-dist': {'id': 'Artistic-dist', 'deprecated': False},
'aspell-ru': {'id': 'Aspell-RU', 'deprecated': False},
'aswf-digital-assets-1.0': {'id': 'ASWF-Digital-Assets-1.0', 'deprecated': False},
'aswf-digital-assets-1.1': {'id': 'ASWF-Digital-Assets-1.1', 'deprecated': False},
'baekmuk': {'id': 'Baekmuk', 'deprecated': False},
'bahyph': {'id': 'Bahyph', 'deprecated': False},
'barr': {'id': 'Barr', 'deprecated': False},
'bcrypt-solar-designer': {'id': 'bcrypt-Solar-Designer', 'deprecated': False},
'beerware': {'id': 'Beerware', 'deprecated': False},
'bitstream-charter': {'id': 'Bitstream-Charter', 'deprecated': False},
'bitstream-vera': {'id': 'Bitstream-Vera', 'deprecated': False},
'bittorrent-1.0': {'id': 'BitTorrent-1.0', 'deprecated': False},
'bittorrent-1.1': {'id': 'BitTorrent-1.1', 'deprecated': False},
'blessing': {'id': 'blessing', 'deprecated': False},
'blueoak-1.0.0': {'id': 'BlueOak-1.0.0', 'deprecated': False},
'boehm-gc': {'id': 'Boehm-GC', 'deprecated': False},
'boehm-gc-without-fee': {'id': 'Boehm-GC-without-fee', 'deprecated': False},
'borceux': {'id': 'Borceux', 'deprecated': False},
'brian-gladman-2-clause': {'id': 'Brian-Gladman-2-Clause', 'deprecated': False},
'brian-gladman-3-clause': {'id': 'Brian-Gladman-3-Clause', 'deprecated': False},
'bsd-1-clause': {'id': 'BSD-1-Clause', 'deprecated': False},
'bsd-2-clause': {'id': 'BSD-2-Clause', 'deprecated': False},
'bsd-2-clause-darwin': {'id': 'BSD-2-Clause-Darwin', 'deprecated': False},
'bsd-2-clause-first-lines': {'id': 'BSD-2-Clause-first-lines', 'deprecated': False},
'bsd-2-clause-freebsd': {'id': 'BSD-2-Clause-FreeBSD', 'deprecated': True},
'bsd-2-clause-netbsd': {'id': 'BSD-2-Clause-NetBSD', 'deprecated': True},
'bsd-2-clause-patent': {'id': 'BSD-2-Clause-Patent', 'deprecated': False},
'bsd-2-clause-pkgconf-disclaimer': {'id': 'BSD-2-Clause-pkgconf-disclaimer', 'deprecated': False},
'bsd-2-clause-views': {'id': 'BSD-2-Clause-Views', 'deprecated': False},
'bsd-3-clause': {'id': 'BSD-3-Clause', 'deprecated': False},
'bsd-3-clause-acpica': {'id': 'BSD-3-Clause-acpica', 'deprecated': False},
'bsd-3-clause-attribution': {'id': 'BSD-3-Clause-Attribution', 'deprecated': False},
'bsd-3-clause-clear': {'id': 'BSD-3-Clause-Clear', 'deprecated': False},
'bsd-3-clause-flex': {'id': 'BSD-3-Clause-flex', 'deprecated': False},
'bsd-3-clause-hp': {'id': 'BSD-3-Clause-HP', 'deprecated': False},
'bsd-3-clause-lbnl': {'id': 'BSD-3-Clause-LBNL', 'deprecated': False},
'bsd-3-clause-modification': {'id': 'BSD-3-Clause-Modification', 'deprecated': False},
'bsd-3-clause-no-military-license': {'id': 'BSD-3-Clause-No-Military-License', 'deprecated': False},
'bsd-3-clause-no-nuclear-license': {'id': 'BSD-3-Clause-No-Nuclear-License', 'deprecated': False},
'bsd-3-clause-no-nuclear-license-2014': {'id': 'BSD-3-Clause-No-Nuclear-License-2014', 'deprecated': False},
'bsd-3-clause-no-nuclear-warranty': {'id': 'BSD-3-Clause-No-Nuclear-Warranty', 'deprecated': False},
'bsd-3-clause-open-mpi': {'id': 'BSD-3-Clause-Open-MPI', 'deprecated': False},
'bsd-3-clause-sun': {'id': 'BSD-3-Clause-Sun', 'deprecated': False},
'bsd-4-clause': {'id': 'BSD-4-Clause', 'deprecated': False},
'bsd-4-clause-shortened': {'id': 'BSD-4-Clause-Shortened', 'deprecated': False},
'bsd-4-clause-uc': {'id': 'BSD-4-Clause-UC', 'deprecated': False},
'bsd-4.3reno': {'id': 'BSD-4.3RENO', 'deprecated': False},
'bsd-4.3tahoe': {'id': 'BSD-4.3TAHOE', 'deprecated': False},
'bsd-advertising-acknowledgement': {'id': 'BSD-Advertising-Acknowledgement', 'deprecated': False},
'bsd-attribution-hpnd-disclaimer': {'id': 'BSD-Attribution-HPND-disclaimer', 'deprecated': False},
'bsd-inferno-nettverk': {'id': 'BSD-Inferno-Nettverk', 'deprecated': False},
'bsd-protection': {'id': 'BSD-Protection', 'deprecated': False},
'bsd-source-beginning-file': {'id': 'BSD-Source-beginning-file', 'deprecated': False},
'bsd-source-code': {'id': 'BSD-Source-Code', 'deprecated': False},
'bsd-systemics': {'id': 'BSD-Systemics', 'deprecated': False},
'bsd-systemics-w3works': {'id': 'BSD-Systemics-W3Works', 'deprecated': False},
'bsl-1.0': {'id': 'BSL-1.0', 'deprecated': False},
'busl-1.1': {'id': 'BUSL-1.1', 'deprecated': False},
'bzip2-1.0.5': {'id': 'bzip2-1.0.5', 'deprecated': True},
'bzip2-1.0.6': {'id': 'bzip2-1.0.6', 'deprecated': False},
'c-uda-1.0': {'id': 'C-UDA-1.0', 'deprecated': False},
'cal-1.0': {'id': 'CAL-1.0', 'deprecated': False},
'cal-1.0-combined-work-exception': {'id': 'CAL-1.0-Combined-Work-Exception', 'deprecated': False},
'caldera': {'id': 'Caldera', 'deprecated': False},
'caldera-no-preamble': {'id': 'Caldera-no-preamble', 'deprecated': False},
'catharon': {'id': 'Catharon', 'deprecated': False},
'catosl-1.1': {'id': 'CATOSL-1.1', 'deprecated': False},
'cc-by-1.0': {'id': 'CC-BY-1.0', 'deprecated': False},
'cc-by-2.0': {'id': 'CC-BY-2.0', 'deprecated': False},
'cc-by-2.5': {'id': 'CC-BY-2.5', 'deprecated': False},
'cc-by-2.5-au': {'id': 'CC-BY-2.5-AU', 'deprecated': False},
'cc-by-3.0': {'id': 'CC-BY-3.0', 'deprecated': False},
'cc-by-3.0-at': {'id': 'CC-BY-3.0-AT', 'deprecated': False},
'cc-by-3.0-au': {'id': 'CC-BY-3.0-AU', 'deprecated': False},
'cc-by-3.0-de': {'id': 'CC-BY-3.0-DE', 'deprecated': False},
'cc-by-3.0-igo': {'id': 'CC-BY-3.0-IGO', 'deprecated': False},
'cc-by-3.0-nl': {'id': 'CC-BY-3.0-NL', 'deprecated': False},
'cc-by-3.0-us': {'id': 'CC-BY-3.0-US', 'deprecated': False},
'cc-by-4.0': {'id': 'CC-BY-4.0', 'deprecated': False},
'cc-by-nc-1.0': {'id': 'CC-BY-NC-1.0', 'deprecated': False},
'cc-by-nc-2.0': {'id': 'CC-BY-NC-2.0', 'deprecated': False},
'cc-by-nc-2.5': {'id': 'CC-BY-NC-2.5', 'deprecated': False},
'cc-by-nc-3.0': {'id': 'CC-BY-NC-3.0', 'deprecated': False},
'cc-by-nc-3.0-de': {'id': 'CC-BY-NC-3.0-DE', 'deprecated': False},
'cc-by-nc-4.0': {'id': 'CC-BY-NC-4.0', 'deprecated': False},
'cc-by-nc-nd-1.0': {'id': 'CC-BY-NC-ND-1.0', 'deprecated': False},
'cc-by-nc-nd-2.0': {'id': 'CC-BY-NC-ND-2.0', 'deprecated': False},
'cc-by-nc-nd-2.5': {'id': 'CC-BY-NC-ND-2.5', 'deprecated': False},
'cc-by-nc-nd-3.0': {'id': 'CC-BY-NC-ND-3.0', 'deprecated': False},
'cc-by-nc-nd-3.0-de': {'id': 'CC-BY-NC-ND-3.0-DE', 'deprecated': False},
'cc-by-nc-nd-3.0-igo': {'id': 'CC-BY-NC-ND-3.0-IGO', 'deprecated': False},
'cc-by-nc-nd-4.0': {'id': 'CC-BY-NC-ND-4.0', 'deprecated': False},
'cc-by-nc-sa-1.0': {'id': 'CC-BY-NC-SA-1.0', 'deprecated': False},
'cc-by-nc-sa-2.0': {'id': 'CC-BY-NC-SA-2.0', 'deprecated': False},
'cc-by-nc-sa-2.0-de': {'id': 'CC-BY-NC-SA-2.0-DE', 'deprecated': False},
'cc-by-nc-sa-2.0-fr': {'id': 'CC-BY-NC-SA-2.0-FR', 'deprecated': False},
'cc-by-nc-sa-2.0-uk': {'id': 'CC-BY-NC-SA-2.0-UK', 'deprecated': False},
'cc-by-nc-sa-2.5': {'id': 'CC-BY-NC-SA-2.5', 'deprecated': False},
'cc-by-nc-sa-3.0': {'id': 'CC-BY-NC-SA-3.0', 'deprecated': False},
'cc-by-nc-sa-3.0-de': {'id': 'CC-BY-NC-SA-3.0-DE', 'deprecated': False},
'cc-by-nc-sa-3.0-igo': {'id': 'CC-BY-NC-SA-3.0-IGO', 'deprecated': False},
'cc-by-nc-sa-4.0': {'id': 'CC-BY-NC-SA-4.0', 'deprecated': False},
'cc-by-nd-1.0': {'id': 'CC-BY-ND-1.0', 'deprecated': False},
'cc-by-nd-2.0': {'id': 'CC-BY-ND-2.0', 'deprecated': False},
'cc-by-nd-2.5': {'id': 'CC-BY-ND-2.5', 'deprecated': False},
'cc-by-nd-3.0': {'id': 'CC-BY-ND-3.0', 'deprecated': False},
'cc-by-nd-3.0-de': {'id': 'CC-BY-ND-3.0-DE', 'deprecated': False},
'cc-by-nd-4.0': {'id': 'CC-BY-ND-4.0', 'deprecated': False},
'cc-by-sa-1.0': {'id': 'CC-BY-SA-1.0', 'deprecated': False},
'cc-by-sa-2.0': {'id': 'CC-BY-SA-2.0', 'deprecated': False},
'cc-by-sa-2.0-uk': {'id': 'CC-BY-SA-2.0-UK', 'deprecated': False},
'cc-by-sa-2.1-jp': {'id': 'CC-BY-SA-2.1-JP', 'deprecated': False},
'cc-by-sa-2.5': {'id': 'CC-BY-SA-2.5', 'deprecated': False},
'cc-by-sa-3.0': {'id': 'CC-BY-SA-3.0', 'deprecated': False},
'cc-by-sa-3.0-at': {'id': 'CC-BY-SA-3.0-AT', 'deprecated': False},
'cc-by-sa-3.0-de': {'id': 'CC-BY-SA-3.0-DE', 'deprecated': False},
'cc-by-sa-3.0-igo': {'id': 'CC-BY-SA-3.0-IGO', 'deprecated': False},
'cc-by-sa-4.0': {'id': 'CC-BY-SA-4.0', 'deprecated': False},
'cc-pddc': {'id': 'CC-PDDC', 'deprecated': False},
'cc-pdm-1.0': {'id': 'CC-PDM-1.0', 'deprecated': False},
'cc-sa-1.0': {'id': 'CC-SA-1.0', 'deprecated': False},
'cc0-1.0': {'id': 'CC0-1.0', 'deprecated': False},
'cddl-1.0': {'id': 'CDDL-1.0', 'deprecated': False},
'cddl-1.1': {'id': 'CDDL-1.1', 'deprecated': False},
'cdl-1.0': {'id': 'CDL-1.0', 'deprecated': False},
'cdla-permissive-1.0': {'id': 'CDLA-Permissive-1.0', 'deprecated': False},
'cdla-permissive-2.0': {'id': 'CDLA-Permissive-2.0', 'deprecated': False},
'cdla-sharing-1.0': {'id': 'CDLA-Sharing-1.0', 'deprecated': False},
'cecill-1.0': {'id': 'CECILL-1.0', 'deprecated': False},
'cecill-1.1': {'id': 'CECILL-1.1', 'deprecated': False},
'cecill-2.0': {'id': 'CECILL-2.0', 'deprecated': False},
'cecill-2.1': {'id': 'CECILL-2.1', 'deprecated': False},
'cecill-b': {'id': 'CECILL-B', 'deprecated': False},
'cecill-c': {'id': 'CECILL-C', 'deprecated': False},
'cern-ohl-1.1': {'id': 'CERN-OHL-1.1', 'deprecated': False},
'cern-ohl-1.2': {'id': 'CERN-OHL-1.2', 'deprecated': False},
'cern-ohl-p-2.0': {'id': 'CERN-OHL-P-2.0', 'deprecated': False},
'cern-ohl-s-2.0': {'id': 'CERN-OHL-S-2.0', 'deprecated': False},
'cern-ohl-w-2.0': {'id': 'CERN-OHL-W-2.0', 'deprecated': False},
'cfitsio': {'id': 'CFITSIO', 'deprecated': False},
'check-cvs': {'id': 'check-cvs', 'deprecated': False},
'checkmk': {'id': 'checkmk', 'deprecated': False},
'clartistic': {'id': 'ClArtistic', 'deprecated': False},
'clips': {'id': 'Clips', 'deprecated': False},
'cmu-mach': {'id': 'CMU-Mach', 'deprecated': False},
'cmu-mach-nodoc': {'id': 'CMU-Mach-nodoc', 'deprecated': False},
'cnri-jython': {'id': 'CNRI-Jython', 'deprecated': False},
'cnri-python': {'id': 'CNRI-Python', 'deprecated': False},
'cnri-python-gpl-compatible': {'id': 'CNRI-Python-GPL-Compatible', 'deprecated': False},
'coil-1.0': {'id': 'COIL-1.0', 'deprecated': False},
'community-spec-1.0': {'id': 'Community-Spec-1.0', 'deprecated': False},
'condor-1.1': {'id': 'Condor-1.1', 'deprecated': False},
'copyleft-next-0.3.0': {'id': 'copyleft-next-0.3.0', 'deprecated': False},
'copyleft-next-0.3.1': {'id': 'copyleft-next-0.3.1', 'deprecated': False},
'cornell-lossless-jpeg': {'id': 'Cornell-Lossless-JPEG', 'deprecated': False},
'cpal-1.0': {'id': 'CPAL-1.0', 'deprecated': False},
'cpl-1.0': {'id': 'CPL-1.0', 'deprecated': False},
'cpol-1.02': {'id': 'CPOL-1.02', 'deprecated': False},
'cronyx': {'id': 'Cronyx', 'deprecated': False},
'crossword': {'id': 'Crossword', 'deprecated': False},
'cryptoswift': {'id': 'CryptoSwift', 'deprecated': False},
'crystalstacker': {'id': 'CrystalStacker', 'deprecated': False},
'cua-opl-1.0': {'id': 'CUA-OPL-1.0', 'deprecated': False},
'cube': {'id': 'Cube', 'deprecated': False},
'curl': {'id': 'curl', 'deprecated': False},
'cve-tou': {'id': 'cve-tou', 'deprecated': False},
'd-fsl-1.0': {'id': 'D-FSL-1.0', 'deprecated': False},
'dec-3-clause': {'id': 'DEC-3-Clause', 'deprecated': False},
'diffmark': {'id': 'diffmark', 'deprecated': False},
'dl-de-by-2.0': {'id': 'DL-DE-BY-2.0', 'deprecated': False},
'dl-de-zero-2.0': {'id': 'DL-DE-ZERO-2.0', 'deprecated': False},
'doc': {'id': 'DOC', 'deprecated': False},
'docbook-dtd': {'id': 'DocBook-DTD', 'deprecated': False},
'docbook-schema': {'id': 'DocBook-Schema', 'deprecated': False},
'docbook-stylesheet': {'id': 'DocBook-Stylesheet', 'deprecated': False},
'docbook-xml': {'id': 'DocBook-XML', 'deprecated': False},
'dotseqn': {'id': 'Dotseqn', 'deprecated': False},
'drl-1.0': {'id': 'DRL-1.0', 'deprecated': False},
'drl-1.1': {'id': 'DRL-1.1', 'deprecated': False},
'dsdp': {'id': 'DSDP', 'deprecated': False},
'dtoa': {'id': 'dtoa', 'deprecated': False},
'dvipdfm': {'id': 'dvipdfm', 'deprecated': False},
'ecl-1.0': {'id': 'ECL-1.0', 'deprecated': False},
'ecl-2.0': {'id': 'ECL-2.0', 'deprecated': False},
'ecos-2.0': {'id': 'eCos-2.0', 'deprecated': True},
'efl-1.0': {'id': 'EFL-1.0', 'deprecated': False},
'efl-2.0': {'id': 'EFL-2.0', 'deprecated': False},
'egenix': {'id': 'eGenix', 'deprecated': False},
'elastic-2.0': {'id': 'Elastic-2.0', 'deprecated': False},
'entessa': {'id': 'Entessa', 'deprecated': False},
'epics': {'id': 'EPICS', 'deprecated': False},
'epl-1.0': {'id': 'EPL-1.0', 'deprecated': False},
'epl-2.0': {'id': 'EPL-2.0', 'deprecated': False},
'erlpl-1.1': {'id': 'ErlPL-1.1', 'deprecated': False},
'etalab-2.0': {'id': 'etalab-2.0', 'deprecated': False},
'eudatagrid': {'id': 'EUDatagrid', 'deprecated': False},
'eupl-1.0': {'id': 'EUPL-1.0', 'deprecated': False},
'eupl-1.1': {'id': 'EUPL-1.1', 'deprecated': False},
'eupl-1.2': {'id': 'EUPL-1.2', 'deprecated': False},
'eurosym': {'id': 'Eurosym', 'deprecated': False},
'fair': {'id': 'Fair', 'deprecated': False},
'fbm': {'id': 'FBM', 'deprecated': False},
'fdk-aac': {'id': 'FDK-AAC', 'deprecated': False},
'ferguson-twofish': {'id': 'Ferguson-Twofish', 'deprecated': False},
'frameworx-1.0': {'id': 'Frameworx-1.0', 'deprecated': False},
'freebsd-doc': {'id': 'FreeBSD-DOC', 'deprecated': False},
'freeimage': {'id': 'FreeImage', 'deprecated': False},
'fsfap': {'id': 'FSFAP', 'deprecated': False},
'fsfap-no-warranty-disclaimer': {'id': 'FSFAP-no-warranty-disclaimer', 'deprecated': False},
'fsful': {'id': 'FSFUL', 'deprecated': False},
'fsfullr': {'id': 'FSFULLR', 'deprecated': False},
'fsfullrsd': {'id': 'FSFULLRSD', 'deprecated': False},
'fsfullrwd': {'id': 'FSFULLRWD', 'deprecated': False},
'fsl-1.1-alv2': {'id': 'FSL-1.1-ALv2', 'deprecated': False},
'fsl-1.1-mit': {'id': 'FSL-1.1-MIT', 'deprecated': False},
'ftl': {'id': 'FTL', 'deprecated': False},
'furuseth': {'id': 'Furuseth', 'deprecated': False},
'fwlw': {'id': 'fwlw', 'deprecated': False},
'game-programming-gems': {'id': 'Game-Programming-Gems', 'deprecated': False},
'gcr-docs': {'id': 'GCR-docs', 'deprecated': False},
'gd': {'id': 'GD', 'deprecated': False},
'generic-xts': {'id': 'generic-xts', 'deprecated': False},
'gfdl-1.1': {'id': 'GFDL-1.1', 'deprecated': True},
'gfdl-1.1-invariants-only': {'id': 'GFDL-1.1-invariants-only', 'deprecated': False},
'gfdl-1.1-invariants-or-later': {'id': 'GFDL-1.1-invariants-or-later', 'deprecated': False},
'gfdl-1.1-no-invariants-only': {'id': 'GFDL-1.1-no-invariants-only', 'deprecated': False},
'gfdl-1.1-no-invariants-or-later': {'id': 'GFDL-1.1-no-invariants-or-later', 'deprecated': False},
'gfdl-1.1-only': {'id': 'GFDL-1.1-only', 'deprecated': False},
'gfdl-1.1-or-later': {'id': 'GFDL-1.1-or-later', 'deprecated': False},
'gfdl-1.2': {'id': 'GFDL-1.2', 'deprecated': True},
'gfdl-1.2-invariants-only': {'id': 'GFDL-1.2-invariants-only', 'deprecated': False},
'gfdl-1.2-invariants-or-later': {'id': 'GFDL-1.2-invariants-or-later', 'deprecated': False},
'gfdl-1.2-no-invariants-only': {'id': 'GFDL-1.2-no-invariants-only', 'deprecated': False},
'gfdl-1.2-no-invariants-or-later': {'id': 'GFDL-1.2-no-invariants-or-later', 'deprecated': False},
'gfdl-1.2-only': {'id': 'GFDL-1.2-only', 'deprecated': False},
'gfdl-1.2-or-later': {'id': 'GFDL-1.2-or-later', 'deprecated': False},
'gfdl-1.3': {'id': 'GFDL-1.3', 'deprecated': True},
'gfdl-1.3-invariants-only': {'id': 'GFDL-1.3-invariants-only', 'deprecated': False},
'gfdl-1.3-invariants-or-later': {'id': 'GFDL-1.3-invariants-or-later', 'deprecated': False},
'gfdl-1.3-no-invariants-only': {'id': 'GFDL-1.3-no-invariants-only', 'deprecated': False},
'gfdl-1.3-no-invariants-or-later': {'id': 'GFDL-1.3-no-invariants-or-later', 'deprecated': False},
'gfdl-1.3-only': {'id': 'GFDL-1.3-only', 'deprecated': False},
'gfdl-1.3-or-later': {'id': 'GFDL-1.3-or-later', 'deprecated': False},
'giftware': {'id': 'Giftware', 'deprecated': False},
'gl2ps': {'id': 'GL2PS', 'deprecated': False},
'glide': {'id': 'Glide', 'deprecated': False},
'glulxe': {'id': 'Glulxe', 'deprecated': False},
'glwtpl': {'id': 'GLWTPL', 'deprecated': False},
'gnuplot': {'id': 'gnuplot', 'deprecated': False},
'gpl-1.0': {'id': 'GPL-1.0', 'deprecated': True},
'gpl-1.0+': {'id': 'GPL-1.0+', 'deprecated': True},
'gpl-1.0-only': {'id': 'GPL-1.0-only', 'deprecated': False},
'gpl-1.0-or-later': {'id': 'GPL-1.0-or-later', 'deprecated': False},
'gpl-2.0': {'id': 'GPL-2.0', 'deprecated': True},
'gpl-2.0+': {'id': 'GPL-2.0+', 'deprecated': True},
'gpl-2.0-only': {'id': 'GPL-2.0-only', 'deprecated': False},
'gpl-2.0-or-later': {'id': 'GPL-2.0-or-later', 'deprecated': False},
'gpl-2.0-with-autoconf-exception': {'id': 'GPL-2.0-with-autoconf-exception', 'deprecated': True},
'gpl-2.0-with-bison-exception': {'id': 'GPL-2.0-with-bison-exception', 'deprecated': True},
'gpl-2.0-with-classpath-exception': {'id': 'GPL-2.0-with-classpath-exception', 'deprecated': True},
'gpl-2.0-with-font-exception': {'id': 'GPL-2.0-with-font-exception', 'deprecated': True},
'gpl-2.0-with-gcc-exception': {'id': 'GPL-2.0-with-GCC-exception', 'deprecated': True},
'gpl-3.0': {'id': 'GPL-3.0', 'deprecated': True},
'gpl-3.0+': {'id': 'GPL-3.0+', 'deprecated': True},
'gpl-3.0-only': {'id': 'GPL-3.0-only', 'deprecated': False},
'gpl-3.0-or-later': {'id': 'GPL-3.0-or-later', 'deprecated': False},
'gpl-3.0-with-autoconf-exception': {'id': 'GPL-3.0-with-autoconf-exception', 'deprecated': True},
'gpl-3.0-with-gcc-exception': {'id': 'GPL-3.0-with-GCC-exception', 'deprecated': True},
'graphics-gems': {'id': 'Graphics-Gems', 'deprecated': False},
'gsoap-1.3b': {'id': 'gSOAP-1.3b', 'deprecated': False},
'gtkbook': {'id': 'gtkbook', 'deprecated': False},
'gutmann': {'id': 'Gutmann', 'deprecated': False},
'haskellreport': {'id': 'HaskellReport', 'deprecated': False},
'hdf5': {'id': 'HDF5', 'deprecated': False},
'hdparm': {'id': 'hdparm', 'deprecated': False},
'hidapi': {'id': 'HIDAPI', 'deprecated': False},
'hippocratic-2.1': {'id': 'Hippocratic-2.1', 'deprecated': False},
'hp-1986': {'id': 'HP-1986', 'deprecated': False},
'hp-1989': {'id': 'HP-1989', 'deprecated': False},
'hpnd': {'id': 'HPND', 'deprecated': False},
'hpnd-dec': {'id': 'HPND-DEC', 'deprecated': False},
'hpnd-doc': {'id': 'HPND-doc', 'deprecated': False},
'hpnd-doc-sell': {'id': 'HPND-doc-sell', 'deprecated': False},
'hpnd-export-us': {'id': 'HPND-export-US', 'deprecated': False},
'hpnd-export-us-acknowledgement': {'id': 'HPND-export-US-acknowledgement', 'deprecated': False},
'hpnd-export-us-modify': {'id': 'HPND-export-US-modify', 'deprecated': False},
'hpnd-export2-us': {'id': 'HPND-export2-US', 'deprecated': False},
'hpnd-fenneberg-livingston': {'id': 'HPND-Fenneberg-Livingston', 'deprecated': False},
'hpnd-inria-imag': {'id': 'HPND-INRIA-IMAG', 'deprecated': False},
'hpnd-intel': {'id': 'HPND-Intel', 'deprecated': False},
'hpnd-kevlin-henney': {'id': 'HPND-Kevlin-Henney', 'deprecated': False},
'hpnd-markus-kuhn': {'id': 'HPND-Markus-Kuhn', 'deprecated': False},
'hpnd-merchantability-variant': {'id': 'HPND-merchantability-variant', 'deprecated': False},
'hpnd-mit-disclaimer': {'id': 'HPND-MIT-disclaimer', 'deprecated': False},
'hpnd-netrek': {'id': 'HPND-Netrek', 'deprecated': False},
'hpnd-pbmplus': {'id': 'HPND-Pbmplus', 'deprecated': False},
'hpnd-sell-mit-disclaimer-xserver': {'id': 'HPND-sell-MIT-disclaimer-xserver', 'deprecated': False},
'hpnd-sell-regexpr': {'id': 'HPND-sell-regexpr', 'deprecated': False},
'hpnd-sell-variant': {'id': 'HPND-sell-variant', 'deprecated': False},
'hpnd-sell-variant-mit-disclaimer': {'id': 'HPND-sell-variant-MIT-disclaimer', 'deprecated': False},
'hpnd-sell-variant-mit-disclaimer-rev': {'id': 'HPND-sell-variant-MIT-disclaimer-rev', 'deprecated': False},
'hpnd-uc': {'id': 'HPND-UC', 'deprecated': False},
'hpnd-uc-export-us': {'id': 'HPND-UC-export-US', 'deprecated': False},
'htmltidy': {'id': 'HTMLTIDY', 'deprecated': False},
'ibm-pibs': {'id': 'IBM-pibs', 'deprecated': False},
'icu': {'id': 'ICU', 'deprecated': False},
'iec-code-components-eula': {'id': 'IEC-Code-Components-EULA', 'deprecated': False},
'ijg': {'id': 'IJG', 'deprecated': False},
'ijg-short': {'id': 'IJG-short', 'deprecated': False},
'imagemagick': {'id': 'ImageMagick', 'deprecated': False},
'imatix': {'id': 'iMatix', 'deprecated': False},
'imlib2': {'id': 'Imlib2', 'deprecated': False},
'info-zip': {'id': 'Info-ZIP', 'deprecated': False},
'inner-net-2.0': {'id': 'Inner-Net-2.0', 'deprecated': False},
'innosetup': {'id': 'InnoSetup', 'deprecated': False},
'intel': {'id': 'Intel', 'deprecated': False},
'intel-acpi': {'id': 'Intel-ACPI', 'deprecated': False},
'interbase-1.0': {'id': 'Interbase-1.0', 'deprecated': False},
'ipa': {'id': 'IPA', 'deprecated': False},
'ipl-1.0': {'id': 'IPL-1.0', 'deprecated': False},
'isc': {'id': 'ISC', 'deprecated': False},
'isc-veillard': {'id': 'ISC-Veillard', 'deprecated': False},
'jam': {'id': 'Jam', 'deprecated': False},
'jasper-2.0': {'id': 'JasPer-2.0', 'deprecated': False},
'jove': {'id': 'jove', 'deprecated': False},
'jpl-image': {'id': 'JPL-image', 'deprecated': False},
'jpnic': {'id': 'JPNIC', 'deprecated': False},
'json': {'id': 'JSON', 'deprecated': False},
'kastrup': {'id': 'Kastrup', 'deprecated': False},
'kazlib': {'id': 'Kazlib', 'deprecated': False},
'knuth-ctan': {'id': 'Knuth-CTAN', 'deprecated': False},
'lal-1.2': {'id': 'LAL-1.2', 'deprecated': False},
'lal-1.3': {'id': 'LAL-1.3', 'deprecated': False},
'latex2e': {'id': 'Latex2e', 'deprecated': False},
'latex2e-translated-notice': {'id': 'Latex2e-translated-notice', 'deprecated': False},
'leptonica': {'id': 'Leptonica', 'deprecated': False},
'lgpl-2.0': {'id': 'LGPL-2.0', 'deprecated': True},
'lgpl-2.0+': {'id': 'LGPL-2.0+', 'deprecated': True},
'lgpl-2.0-only': {'id': 'LGPL-2.0-only', 'deprecated': False},
'lgpl-2.0-or-later': {'id': 'LGPL-2.0-or-later', 'deprecated': False},
'lgpl-2.1': {'id': 'LGPL-2.1', 'deprecated': True},
'lgpl-2.1+': {'id': 'LGPL-2.1+', 'deprecated': True},
'lgpl-2.1-only': {'id': 'LGPL-2.1-only', 'deprecated': False},
'lgpl-2.1-or-later': {'id': 'LGPL-2.1-or-later', 'deprecated': False},
'lgpl-3.0': {'id': 'LGPL-3.0', 'deprecated': True},
'lgpl-3.0+': {'id': 'LGPL-3.0+', 'deprecated': True},
'lgpl-3.0-only': {'id': 'LGPL-3.0-only', 'deprecated': False},
'lgpl-3.0-or-later': {'id': 'LGPL-3.0-or-later', 'deprecated': False},
'lgpllr': {'id': 'LGPLLR', 'deprecated': False},
'libpng': {'id': 'Libpng', 'deprecated': False},
'libpng-1.6.35': {'id': 'libpng-1.6.35', 'deprecated': False},
'libpng-2.0': {'id': 'libpng-2.0', 'deprecated': False},
'libselinux-1.0': {'id': 'libselinux-1.0', 'deprecated': False},
'libtiff': {'id': 'libtiff', 'deprecated': False},
'libutil-david-nugent': {'id': 'libutil-David-Nugent', 'deprecated': False},
'liliq-p-1.1': {'id': 'LiLiQ-P-1.1', 'deprecated': False},
'liliq-r-1.1': {'id': 'LiLiQ-R-1.1', 'deprecated': False},
'liliq-rplus-1.1': {'id': 'LiLiQ-Rplus-1.1', 'deprecated': False},
'linux-man-pages-1-para': {'id': 'Linux-man-pages-1-para', 'deprecated': False},
'linux-man-pages-copyleft': {'id': 'Linux-man-pages-copyleft', 'deprecated': False},
'linux-man-pages-copyleft-2-para': {'id': 'Linux-man-pages-copyleft-2-para', 'deprecated': False},
'linux-man-pages-copyleft-var': {'id': 'Linux-man-pages-copyleft-var', 'deprecated': False},
'linux-openib': {'id': 'Linux-OpenIB', 'deprecated': False},
'loop': {'id': 'LOOP', 'deprecated': False},
'lpd-document': {'id': 'LPD-document', 'deprecated': False},
'lpl-1.0': {'id': 'LPL-1.0', 'deprecated': False},
'lpl-1.02': {'id': 'LPL-1.02', 'deprecated': False},
'lppl-1.0': {'id': 'LPPL-1.0', 'deprecated': False},
'lppl-1.1': {'id': 'LPPL-1.1', 'deprecated': False},
'lppl-1.2': {'id': 'LPPL-1.2', 'deprecated': False},
'lppl-1.3a': {'id': 'LPPL-1.3a', 'deprecated': False},
'lppl-1.3c': {'id': 'LPPL-1.3c', 'deprecated': False},
'lsof': {'id': 'lsof', 'deprecated': False},
'lucida-bitmap-fonts': {'id': 'Lucida-Bitmap-Fonts', 'deprecated': False},
'lzma-sdk-9.11-to-9.20': {'id': 'LZMA-SDK-9.11-to-9.20', 'deprecated': False},
'lzma-sdk-9.22': {'id': 'LZMA-SDK-9.22', 'deprecated': False},
'mackerras-3-clause': {'id': 'Mackerras-3-Clause', 'deprecated': False},
'mackerras-3-clause-acknowledgment': {'id': 'Mackerras-3-Clause-acknowledgment', 'deprecated': False},
'magaz': {'id': 'magaz', 'deprecated': False},
'mailprio': {'id': 'mailprio', 'deprecated': False},
'makeindex': {'id': 'MakeIndex', 'deprecated': False},
'man2html': {'id': 'man2html', 'deprecated': False},
'martin-birgmeier': {'id': 'Martin-Birgmeier', 'deprecated': False},
'mcphee-slideshow': {'id': 'McPhee-slideshow', 'deprecated': False},
'metamail': {'id': 'metamail', 'deprecated': False},
'minpack': {'id': 'Minpack', 'deprecated': False},
'mips': {'id': 'MIPS', 'deprecated': False},
'miros': {'id': 'MirOS', 'deprecated': False},
'mit': {'id': 'MIT', 'deprecated': False},
'mit-0': {'id': 'MIT-0', 'deprecated': False},
'mit-advertising': {'id': 'MIT-advertising', 'deprecated': False},
'mit-click': {'id': 'MIT-Click', 'deprecated': False},
'mit-cmu': {'id': 'MIT-CMU', 'deprecated': False},
'mit-enna': {'id': 'MIT-enna', 'deprecated': False},
'mit-feh': {'id': 'MIT-feh', 'deprecated': False},
'mit-festival': {'id': 'MIT-Festival', 'deprecated': False},
'mit-khronos-old': {'id': 'MIT-Khronos-old', 'deprecated': False},
'mit-modern-variant': {'id': 'MIT-Modern-Variant', 'deprecated': False},
'mit-open-group': {'id': 'MIT-open-group', 'deprecated': False},
'mit-testregex': {'id': 'MIT-testregex', 'deprecated': False},
'mit-wu': {'id': 'MIT-Wu', 'deprecated': False},
'mitnfa': {'id': 'MITNFA', 'deprecated': False},
'mmixware': {'id': 'MMIXware', 'deprecated': False},
'motosoto': {'id': 'Motosoto', 'deprecated': False},
'mpeg-ssg': {'id': 'MPEG-SSG', 'deprecated': False},
'mpi-permissive': {'id': 'mpi-permissive', 'deprecated': False},
'mpich2': {'id': 'mpich2', 'deprecated': False},
'mpl-1.0': {'id': 'MPL-1.0', 'deprecated': False},
'mpl-1.1': {'id': 'MPL-1.1', 'deprecated': False},
'mpl-2.0': {'id': 'MPL-2.0', 'deprecated': False},
'mpl-2.0-no-copyleft-exception': {'id': 'MPL-2.0-no-copyleft-exception', 'deprecated': False},
'mplus': {'id': 'mplus', 'deprecated': False},
'ms-lpl': {'id': 'MS-LPL', 'deprecated': False},
'ms-pl': {'id': 'MS-PL', 'deprecated': False},
'ms-rl': {'id': 'MS-RL', 'deprecated': False},
'mtll': {'id': 'MTLL', 'deprecated': False},
'mulanpsl-1.0': {'id': 'MulanPSL-1.0', 'deprecated': False},
'mulanpsl-2.0': {'id': 'MulanPSL-2.0', 'deprecated': False},
'multics': {'id': 'Multics', 'deprecated': False},
'mup': {'id': 'Mup', 'deprecated': False},
'naist-2003': {'id': 'NAIST-2003', 'deprecated': False},
'nasa-1.3': {'id': 'NASA-1.3', 'deprecated': False},
'naumen': {'id': 'Naumen', 'deprecated': False},
'nbpl-1.0': {'id': 'NBPL-1.0', 'deprecated': False},
'ncbi-pd': {'id': 'NCBI-PD', 'deprecated': False},
'ncgl-uk-2.0': {'id': 'NCGL-UK-2.0', 'deprecated': False},
'ncl': {'id': 'NCL', 'deprecated': False},
'ncsa': {'id': 'NCSA', 'deprecated': False},
'net-snmp': {'id': 'Net-SNMP', 'deprecated': True},
'netcdf': {'id': 'NetCDF', 'deprecated': False},
'newsletr': {'id': 'Newsletr', 'deprecated': False},
'ngpl': {'id': 'NGPL', 'deprecated': False},
'ngrep': {'id': 'ngrep', 'deprecated': False},
'nicta-1.0': {'id': 'NICTA-1.0', 'deprecated': False},
'nist-pd': {'id': 'NIST-PD', 'deprecated': False},
'nist-pd-fallback': {'id': 'NIST-PD-fallback', 'deprecated': False},
'nist-software': {'id': 'NIST-Software', 'deprecated': False},
'nlod-1.0': {'id': 'NLOD-1.0', 'deprecated': False},
'nlod-2.0': {'id': 'NLOD-2.0', 'deprecated': False},
'nlpl': {'id': 'NLPL', 'deprecated': False},
'nokia': {'id': 'Nokia', 'deprecated': False},
'nosl': {'id': 'NOSL', 'deprecated': False},
'noweb': {'id': 'Noweb', 'deprecated': False},
'npl-1.0': {'id': 'NPL-1.0', 'deprecated': False},
'npl-1.1': {'id': 'NPL-1.1', 'deprecated': False},
'nposl-3.0': {'id': 'NPOSL-3.0', 'deprecated': False},
'nrl': {'id': 'NRL', 'deprecated': False},
'ntia-pd': {'id': 'NTIA-PD', 'deprecated': False},
'ntp': {'id': 'NTP', 'deprecated': False},
'ntp-0': {'id': 'NTP-0', 'deprecated': False},
'nunit': {'id': 'Nunit', 'deprecated': True},
'o-uda-1.0': {'id': 'O-UDA-1.0', 'deprecated': False},
'oar': {'id': 'OAR', 'deprecated': False},
'occt-pl': {'id': 'OCCT-PL', 'deprecated': False},
'oclc-2.0': {'id': 'OCLC-2.0', 'deprecated': False},
'odbl-1.0': {'id': 'ODbL-1.0', 'deprecated': False},
'odc-by-1.0': {'id': 'ODC-By-1.0', 'deprecated': False},
'offis': {'id': 'OFFIS', 'deprecated': False},
'ofl-1.0': {'id': 'OFL-1.0', 'deprecated': False},
'ofl-1.0-no-rfn': {'id': 'OFL-1.0-no-RFN', 'deprecated': False},
'ofl-1.0-rfn': {'id': 'OFL-1.0-RFN', 'deprecated': False},
'ofl-1.1': {'id': 'OFL-1.1', 'deprecated': False},
'ofl-1.1-no-rfn': {'id': 'OFL-1.1-no-RFN', 'deprecated': False},
'ofl-1.1-rfn': {'id': 'OFL-1.1-RFN', 'deprecated': False},
'ogc-1.0': {'id': 'OGC-1.0', 'deprecated': False},
'ogdl-taiwan-1.0': {'id': 'OGDL-Taiwan-1.0', 'deprecated': False},
'ogl-canada-2.0': {'id': 'OGL-Canada-2.0', 'deprecated': False},
'ogl-uk-1.0': {'id': 'OGL-UK-1.0', 'deprecated': False},
'ogl-uk-2.0': {'id': 'OGL-UK-2.0', 'deprecated': False},
'ogl-uk-3.0': {'id': 'OGL-UK-3.0', 'deprecated': False},
'ogtsl': {'id': 'OGTSL', 'deprecated': False},
'oldap-1.1': {'id': 'OLDAP-1.1', 'deprecated': False},
'oldap-1.2': {'id': 'OLDAP-1.2', 'deprecated': False},
'oldap-1.3': {'id': 'OLDAP-1.3', 'deprecated': False},
'oldap-1.4': {'id': 'OLDAP-1.4', 'deprecated': False},
'oldap-2.0': {'id': 'OLDAP-2.0', 'deprecated': False},
'oldap-2.0.1': {'id': 'OLDAP-2.0.1', 'deprecated': False},
'oldap-2.1': {'id': 'OLDAP-2.1', 'deprecated': False},
'oldap-2.2': {'id': 'OLDAP-2.2', 'deprecated': False},
'oldap-2.2.1': {'id': 'OLDAP-2.2.1', 'deprecated': False},
'oldap-2.2.2': {'id': 'OLDAP-2.2.2', 'deprecated': False},
'oldap-2.3': {'id': 'OLDAP-2.3', 'deprecated': False},
'oldap-2.4': {'id': 'OLDAP-2.4', 'deprecated': False},
'oldap-2.5': {'id': 'OLDAP-2.5', 'deprecated': False},
'oldap-2.6': {'id': 'OLDAP-2.6', 'deprecated': False},
'oldap-2.7': {'id': 'OLDAP-2.7', 'deprecated': False},
'oldap-2.8': {'id': 'OLDAP-2.8', 'deprecated': False},
'olfl-1.3': {'id': 'OLFL-1.3', 'deprecated': False},
'oml': {'id': 'OML', 'deprecated': False},
'openpbs-2.3': {'id': 'OpenPBS-2.3', 'deprecated': False},
'openssl': {'id': 'OpenSSL', 'deprecated': False},
'openssl-standalone': {'id': 'OpenSSL-standalone', 'deprecated': False},
'openvision': {'id': 'OpenVision', 'deprecated': False},
'opl-1.0': {'id': 'OPL-1.0', 'deprecated': False},
'opl-uk-3.0': {'id': 'OPL-UK-3.0', 'deprecated': False},
'opubl-1.0': {'id': 'OPUBL-1.0', 'deprecated': False},
'oset-pl-2.1': {'id': 'OSET-PL-2.1', 'deprecated': False},
'osl-1.0': {'id': 'OSL-1.0', 'deprecated': False},
'osl-1.1': {'id': 'OSL-1.1', 'deprecated': False},
'osl-2.0': {'id': 'OSL-2.0', 'deprecated': False},
'osl-2.1': {'id': 'OSL-2.1', 'deprecated': False},
'osl-3.0': {'id': 'OSL-3.0', 'deprecated': False},
'padl': {'id': 'PADL', 'deprecated': False},
'parity-6.0.0': {'id': 'Parity-6.0.0', 'deprecated': False},
'parity-7.0.0': {'id': 'Parity-7.0.0', 'deprecated': False},
'pddl-1.0': {'id': 'PDDL-1.0', 'deprecated': False},
'php-3.0': {'id': 'PHP-3.0', 'deprecated': False},
'php-3.01': {'id': 'PHP-3.01', 'deprecated': False},
'pixar': {'id': 'Pixar', 'deprecated': False},
'pkgconf': {'id': 'pkgconf', 'deprecated': False},
'plexus': {'id': 'Plexus', 'deprecated': False},
'pnmstitch': {'id': 'pnmstitch', 'deprecated': False},
'polyform-noncommercial-1.0.0': {'id': 'PolyForm-Noncommercial-1.0.0', 'deprecated': False},
'polyform-small-business-1.0.0': {'id': 'PolyForm-Small-Business-1.0.0', 'deprecated': False},
'postgresql': {'id': 'PostgreSQL', 'deprecated': False},
'ppl': {'id': 'PPL', 'deprecated': False},
'psf-2.0': {'id': 'PSF-2.0', 'deprecated': False},
'psfrag': {'id': 'psfrag', 'deprecated': False},
'psutils': {'id': 'psutils', 'deprecated': False},
'python-2.0': {'id': 'Python-2.0', 'deprecated': False},
'python-2.0.1': {'id': 'Python-2.0.1', 'deprecated': False},
'python-ldap': {'id': 'python-ldap', 'deprecated': False},
'qhull': {'id': 'Qhull', 'deprecated': False},
'qpl-1.0': {'id': 'QPL-1.0', 'deprecated': False},
'qpl-1.0-inria-2004': {'id': 'QPL-1.0-INRIA-2004', 'deprecated': False},
'radvd': {'id': 'radvd', 'deprecated': False},
'rdisc': {'id': 'Rdisc', 'deprecated': False},
'rhecos-1.1': {'id': 'RHeCos-1.1', 'deprecated': False},
'rpl-1.1': {'id': 'RPL-1.1', 'deprecated': False},
'rpl-1.5': {'id': 'RPL-1.5', 'deprecated': False},
'rpsl-1.0': {'id': 'RPSL-1.0', 'deprecated': False},
'rsa-md': {'id': 'RSA-MD', 'deprecated': False},
'rscpl': {'id': 'RSCPL', 'deprecated': False},
'ruby': {'id': 'Ruby', 'deprecated': False},
'ruby-pty': {'id': 'Ruby-pty', 'deprecated': False},
'sax-pd': {'id': 'SAX-PD', 'deprecated': False},
'sax-pd-2.0': {'id': 'SAX-PD-2.0', 'deprecated': False},
'saxpath': {'id': 'Saxpath', 'deprecated': False},
'scea': {'id': 'SCEA', 'deprecated': False},
'schemereport': {'id': 'SchemeReport', 'deprecated': False},
'sendmail': {'id': 'Sendmail', 'deprecated': False},
'sendmail-8.23': {'id': 'Sendmail-8.23', 'deprecated': False},
'sendmail-open-source-1.1': {'id': 'Sendmail-Open-Source-1.1', 'deprecated': False},
'sgi-b-1.0': {'id': 'SGI-B-1.0', 'deprecated': False},
'sgi-b-1.1': {'id': 'SGI-B-1.1', 'deprecated': False},
'sgi-b-2.0': {'id': 'SGI-B-2.0', 'deprecated': False},
'sgi-opengl': {'id': 'SGI-OpenGL', 'deprecated': False},
'sgp4': {'id': 'SGP4', 'deprecated': False},
'shl-0.5': {'id': 'SHL-0.5', 'deprecated': False},
'shl-0.51': {'id': 'SHL-0.51', 'deprecated': False},
'simpl-2.0': {'id': 'SimPL-2.0', 'deprecated': False},
'sissl': {'id': 'SISSL', 'deprecated': False},
'sissl-1.2': {'id': 'SISSL-1.2', 'deprecated': False},
'sl': {'id': 'SL', 'deprecated': False},
'sleepycat': {'id': 'Sleepycat', 'deprecated': False},
'smail-gpl': {'id': 'SMAIL-GPL', 'deprecated': False},
'smlnj': {'id': 'SMLNJ', 'deprecated': False},
'smppl': {'id': 'SMPPL', 'deprecated': False},
'snia': {'id': 'SNIA', 'deprecated': False},
'snprintf': {'id': 'snprintf', 'deprecated': False},
'sofa': {'id': 'SOFA', 'deprecated': False},
'softsurfer': {'id': 'softSurfer', 'deprecated': False},
'soundex': {'id': 'Soundex', 'deprecated': False},
'spencer-86': {'id': 'Spencer-86', 'deprecated': False},
'spencer-94': {'id': 'Spencer-94', 'deprecated': False},
'spencer-99': {'id': 'Spencer-99', 'deprecated': False},
'spl-1.0': {'id': 'SPL-1.0', 'deprecated': False},
'ssh-keyscan': {'id': 'ssh-keyscan', 'deprecated': False},
'ssh-openssh': {'id': 'SSH-OpenSSH', 'deprecated': False},
'ssh-short': {'id': 'SSH-short', 'deprecated': False},
'ssleay-standalone': {'id': 'SSLeay-standalone', 'deprecated': False},
'sspl-1.0': {'id': 'SSPL-1.0', 'deprecated': False},
'standardml-nj': {'id': 'StandardML-NJ', 'deprecated': True},
'sugarcrm-1.1.3': {'id': 'SugarCRM-1.1.3', 'deprecated': False},
'sul-1.0': {'id': 'SUL-1.0', 'deprecated': False},
'sun-ppp': {'id': 'Sun-PPP', 'deprecated': False},
'sun-ppp-2000': {'id': 'Sun-PPP-2000', 'deprecated': False},
'sunpro': {'id': 'SunPro', 'deprecated': False},
'swl': {'id': 'SWL', 'deprecated': False},
'swrule': {'id': 'swrule', 'deprecated': False},
'symlinks': {'id': 'Symlinks', 'deprecated': False},
'tapr-ohl-1.0': {'id': 'TAPR-OHL-1.0', 'deprecated': False},
'tcl': {'id': 'TCL', 'deprecated': False},
'tcp-wrappers': {'id': 'TCP-wrappers', 'deprecated': False},
'termreadkey': {'id': 'TermReadKey', 'deprecated': False},
'tgppl-1.0': {'id': 'TGPPL-1.0', 'deprecated': False},
'thirdeye': {'id': 'ThirdEye', 'deprecated': False},
'threeparttable': {'id': 'threeparttable', 'deprecated': False},
'tmate': {'id': 'TMate', 'deprecated': False},
'torque-1.1': {'id': 'TORQUE-1.1', 'deprecated': False},
'tosl': {'id': 'TOSL', 'deprecated': False},
'tpdl': {'id': 'TPDL', 'deprecated': False},
'tpl-1.0': {'id': 'TPL-1.0', 'deprecated': False},
'trustedqsl': {'id': 'TrustedQSL', 'deprecated': False},
'ttwl': {'id': 'TTWL', 'deprecated': False},
'ttyp0': {'id': 'TTYP0', 'deprecated': False},
'tu-berlin-1.0': {'id': 'TU-Berlin-1.0', 'deprecated': False},
'tu-berlin-2.0': {'id': 'TU-Berlin-2.0', 'deprecated': False},
'ubuntu-font-1.0': {'id': 'Ubuntu-font-1.0', 'deprecated': False},
'ucar': {'id': 'UCAR', 'deprecated': False},
'ucl-1.0': {'id': 'UCL-1.0', 'deprecated': False},
'ulem': {'id': 'ulem', 'deprecated': False},
'umich-merit': {'id': 'UMich-Merit', 'deprecated': False},
'unicode-3.0': {'id': 'Unicode-3.0', 'deprecated': False},
'unicode-dfs-2015': {'id': 'Unicode-DFS-2015', 'deprecated': False},
'unicode-dfs-2016': {'id': 'Unicode-DFS-2016', 'deprecated': False},
'unicode-tou': {'id': 'Unicode-TOU', 'deprecated': False},
'unixcrypt': {'id': 'UnixCrypt', 'deprecated': False},
'unlicense': {'id': 'Unlicense', 'deprecated': False},
'unlicense-libtelnet': {'id': 'Unlicense-libtelnet', 'deprecated': False},
'unlicense-libwhirlpool': {'id': 'Unlicense-libwhirlpool', 'deprecated': False},
'upl-1.0': {'id': 'UPL-1.0', 'deprecated': False},
'urt-rle': {'id': 'URT-RLE', 'deprecated': False},
'vim': {'id': 'Vim', 'deprecated': False},
'vostrom': {'id': 'VOSTROM', 'deprecated': False},
'vsl-1.0': {'id': 'VSL-1.0', 'deprecated': False},
'w3c': {'id': 'W3C', 'deprecated': False},
'w3c-19980720': {'id': 'W3C-19980720', 'deprecated': False},
'w3c-20150513': {'id': 'W3C-20150513', 'deprecated': False},
'w3m': {'id': 'w3m', 'deprecated': False},
'watcom-1.0': {'id': 'Watcom-1.0', 'deprecated': False},
'widget-workshop': {'id': 'Widget-Workshop', 'deprecated': False},
'wsuipa': {'id': 'Wsuipa', 'deprecated': False},
'wtfpl': {'id': 'WTFPL', 'deprecated': False},
'wwl': {'id': 'wwl', 'deprecated': False},
'wxwindows': {'id': 'wxWindows', 'deprecated': True},
'x11': {'id': 'X11', 'deprecated': False},
'x11-distribute-modifications-variant': {'id': 'X11-distribute-modifications-variant', 'deprecated': False},
'x11-swapped': {'id': 'X11-swapped', 'deprecated': False},
'xdebug-1.03': {'id': 'Xdebug-1.03', 'deprecated': False},
'xerox': {'id': 'Xerox', 'deprecated': False},
'xfig': {'id': 'Xfig', 'deprecated': False},
'xfree86-1.1': {'id': 'XFree86-1.1', 'deprecated': False},
'xinetd': {'id': 'xinetd', 'deprecated': False},
'xkeyboard-config-zinoviev': {'id': 'xkeyboard-config-Zinoviev', 'deprecated': False},
'xlock': {'id': 'xlock', 'deprecated': False},
'xnet': {'id': 'Xnet', 'deprecated': False},
'xpp': {'id': 'xpp', 'deprecated': False},
'xskat': {'id': 'XSkat', 'deprecated': False},
'xzoom': {'id': 'xzoom', 'deprecated': False},
'ypl-1.0': {'id': 'YPL-1.0', 'deprecated': False},
'ypl-1.1': {'id': 'YPL-1.1', 'deprecated': False},
'zed': {'id': 'Zed', 'deprecated': False},
'zeeff': {'id': 'Zeeff', 'deprecated': False},
'zend-2.0': {'id': 'Zend-2.0', 'deprecated': False},
'zimbra-1.3': {'id': 'Zimbra-1.3', 'deprecated': False},
'zimbra-1.4': {'id': 'Zimbra-1.4', 'deprecated': False},
'zlib': {'id': 'Zlib', 'deprecated': False},
'zlib-acknowledgement': {'id': 'zlib-acknowledgement', 'deprecated': False},
'zpl-1.1': {'id': 'ZPL-1.1', 'deprecated': False},
'zpl-2.0': {'id': 'ZPL-2.0', 'deprecated': False},
'zpl-2.1': {'id': 'ZPL-2.1', 'deprecated': False},
}
EXCEPTIONS: dict[str, SPDXException] = {
'389-exception': {'id': '389-exception', 'deprecated': False},
'asterisk-exception': {'id': 'Asterisk-exception', 'deprecated': False},
'asterisk-linking-protocols-exception': {'id': 'Asterisk-linking-protocols-exception', 'deprecated': False},
'autoconf-exception-2.0': {'id': 'Autoconf-exception-2.0', 'deprecated': False},
'autoconf-exception-3.0': {'id': 'Autoconf-exception-3.0', 'deprecated': False},
'autoconf-exception-generic': {'id': 'Autoconf-exception-generic', 'deprecated': False},
'autoconf-exception-generic-3.0': {'id': 'Autoconf-exception-generic-3.0', 'deprecated': False},
'autoconf-exception-macro': {'id': 'Autoconf-exception-macro', 'deprecated': False},
'bison-exception-1.24': {'id': 'Bison-exception-1.24', 'deprecated': False},
'bison-exception-2.2': {'id': 'Bison-exception-2.2', 'deprecated': False},
'bootloader-exception': {'id': 'Bootloader-exception', 'deprecated': False},
'cgal-linking-exception': {'id': 'CGAL-linking-exception', 'deprecated': False},
'classpath-exception-2.0': {'id': 'Classpath-exception-2.0', 'deprecated': False},
'clisp-exception-2.0': {'id': 'CLISP-exception-2.0', 'deprecated': False},
'cryptsetup-openssl-exception': {'id': 'cryptsetup-OpenSSL-exception', 'deprecated': False},
'digia-qt-lgpl-exception-1.1': {'id': 'Digia-Qt-LGPL-exception-1.1', 'deprecated': False},
'digirule-foss-exception': {'id': 'DigiRule-FOSS-exception', 'deprecated': False},
'ecos-exception-2.0': {'id': 'eCos-exception-2.0', 'deprecated': False},
'erlang-otp-linking-exception': {'id': 'erlang-otp-linking-exception', 'deprecated': False},
'fawkes-runtime-exception': {'id': 'Fawkes-Runtime-exception', 'deprecated': False},
'fltk-exception': {'id': 'FLTK-exception', 'deprecated': False},
'fmt-exception': {'id': 'fmt-exception', 'deprecated': False},
'font-exception-2.0': {'id': 'Font-exception-2.0', 'deprecated': False},
'freertos-exception-2.0': {'id': 'freertos-exception-2.0', 'deprecated': False},
'gcc-exception-2.0': {'id': 'GCC-exception-2.0', 'deprecated': False},
'gcc-exception-2.0-note': {'id': 'GCC-exception-2.0-note', 'deprecated': False},
'gcc-exception-3.1': {'id': 'GCC-exception-3.1', 'deprecated': False},
'gmsh-exception': {'id': 'Gmsh-exception', 'deprecated': False},
'gnat-exception': {'id': 'GNAT-exception', 'deprecated': False},
'gnome-examples-exception': {'id': 'GNOME-examples-exception', 'deprecated': False},
'gnu-compiler-exception': {'id': 'GNU-compiler-exception', 'deprecated': False},
'gnu-javamail-exception': {'id': 'gnu-javamail-exception', 'deprecated': False},
'gpl-3.0-389-ds-base-exception': {'id': 'GPL-3.0-389-ds-base-exception', 'deprecated': False},
'gpl-3.0-interface-exception': {'id': 'GPL-3.0-interface-exception', 'deprecated': False},
'gpl-3.0-linking-exception': {'id': 'GPL-3.0-linking-exception', 'deprecated': False},
'gpl-3.0-linking-source-exception': {'id': 'GPL-3.0-linking-source-exception', 'deprecated': False},
'gpl-cc-1.0': {'id': 'GPL-CC-1.0', 'deprecated': False},
'gstreamer-exception-2005': {'id': 'GStreamer-exception-2005', 'deprecated': False},
'gstreamer-exception-2008': {'id': 'GStreamer-exception-2008', 'deprecated': False},
'harbour-exception': {'id': 'harbour-exception', 'deprecated': False},
'i2p-gpl-java-exception': {'id': 'i2p-gpl-java-exception', 'deprecated': False},
'independent-modules-exception': {'id': 'Independent-modules-exception', 'deprecated': False},
'kicad-libraries-exception': {'id': 'KiCad-libraries-exception', 'deprecated': False},
'lgpl-3.0-linking-exception': {'id': 'LGPL-3.0-linking-exception', 'deprecated': False},
'libpri-openh323-exception': {'id': 'libpri-OpenH323-exception', 'deprecated': False},
'libtool-exception': {'id': 'Libtool-exception', 'deprecated': False},
'linux-syscall-note': {'id': 'Linux-syscall-note', 'deprecated': False},
'llgpl': {'id': 'LLGPL', 'deprecated': False},
'llvm-exception': {'id': 'LLVM-exception', 'deprecated': False},
'lzma-exception': {'id': 'LZMA-exception', 'deprecated': False},
'mif-exception': {'id': 'mif-exception', 'deprecated': False},
'mxml-exception': {'id': 'mxml-exception', 'deprecated': False},
'nokia-qt-exception-1.1': {'id': 'Nokia-Qt-exception-1.1', 'deprecated': True},
'ocaml-lgpl-linking-exception': {'id': 'OCaml-LGPL-linking-exception', 'deprecated': False},
'occt-exception-1.0': {'id': 'OCCT-exception-1.0', 'deprecated': False},
'openjdk-assembly-exception-1.0': {'id': 'OpenJDK-assembly-exception-1.0', 'deprecated': False},
'openvpn-openssl-exception': {'id': 'openvpn-openssl-exception', 'deprecated': False},
'pcre2-exception': {'id': 'PCRE2-exception', 'deprecated': False},
'polyparse-exception': {'id': 'polyparse-exception', 'deprecated': False},
'ps-or-pdf-font-exception-20170817': {'id': 'PS-or-PDF-font-exception-20170817', 'deprecated': False},
'qpl-1.0-inria-2004-exception': {'id': 'QPL-1.0-INRIA-2004-exception', 'deprecated': False},
'qt-gpl-exception-1.0': {'id': 'Qt-GPL-exception-1.0', 'deprecated': False},
'qt-lgpl-exception-1.1': {'id': 'Qt-LGPL-exception-1.1', 'deprecated': False},
'qwt-exception-1.0': {'id': 'Qwt-exception-1.0', 'deprecated': False},
'romic-exception': {'id': 'romic-exception', 'deprecated': False},
'rrdtool-floss-exception-2.0': {'id': 'RRDtool-FLOSS-exception-2.0', 'deprecated': False},
'sane-exception': {'id': 'SANE-exception', 'deprecated': False},
'shl-2.0': {'id': 'SHL-2.0', 'deprecated': False},
'shl-2.1': {'id': 'SHL-2.1', 'deprecated': False},
'stunnel-exception': {'id': 'stunnel-exception', 'deprecated': False},
'swi-exception': {'id': 'SWI-exception', 'deprecated': False},
'swift-exception': {'id': 'Swift-exception', 'deprecated': False},
'texinfo-exception': {'id': 'Texinfo-exception', 'deprecated': False},
'u-boot-exception-2.0': {'id': 'u-boot-exception-2.0', 'deprecated': False},
'ubdl-exception': {'id': 'UBDL-exception', 'deprecated': False},
'universal-foss-exception-1.0': {'id': 'Universal-FOSS-exception-1.0', 'deprecated': False},
'vsftpd-openssl-exception': {'id': 'vsftpd-openssl-exception', 'deprecated': False},
'wxwindows-exception-3.1': {'id': 'WxWindows-exception-3.1', 'deprecated': False},
'x11vnc-openssl-exception': {'id': 'x11vnc-openssl-exception', 'deprecated': False},
}
@@ -0,0 +1,576 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
from __future__ import annotations
import functools
import operator
import os
import platform
import sys
from collections.abc import Set as AbstractSet
from typing import TYPE_CHECKING, Callable, Literal, TypedDict, Union, cast
from ._parser import MarkerAtom, MarkerList, Op, Value, Variable
from ._parser import parse_marker as _parse_marker
from ._tokenizer import ParserSyntaxError
from .specifiers import InvalidSpecifier, Specifier
from .utils import canonicalize_name
if TYPE_CHECKING:
from collections.abc import Mapping
__all__ = [
"Environment",
"EvaluateContext",
"InvalidMarker",
"Marker",
"UndefinedComparison",
"UndefinedEnvironmentName",
"default_environment",
]
def __dir__() -> list[str]:
return __all__
Operator = Callable[[str, Union[str, AbstractSet[str]]], bool]
EvaluateContext = Literal["metadata", "lock_file", "requirement"]
"""A ``typing.Literal`` enumerating valid marker evaluation contexts.
Valid values for the ``context`` passed to :meth:`Marker.evaluate` are:
* ``"metadata"`` (for core metadata; default)
* ``"lock_file"`` (for lock files)
* ``"requirement"`` (i.e. all other situations)
.. versionadded:: 25.0
"""
MARKERS_ALLOWING_SET = {"extras", "dependency_groups"}
MARKERS_REQUIRING_VERSION = {
"implementation_version",
"platform_release",
"python_full_version",
"python_version",
}
class InvalidMarker(ValueError):
"""Raised when attempting to create a :class:`Marker` from invalid input.
This error indicates that the given marker string does not conform to the
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
"""
class UndefinedComparison(ValueError):
"""Raised when evaluating an unsupported marker comparison.
This can happen when marker values are compared as versions but do not
conform to the :ref:`specification of version specifiers
<pypug:version-specifiers>`.
"""
class UndefinedEnvironmentName(KeyError):
"""Raised when evaluating a marker that references a missing environment key.
Subclasses :class:`KeyError` so that code catching the bare ``KeyError`` that
a missing environment lookup historically produced keeps working.
.. versionchanged:: 26.3
Now subclasses :class:`KeyError` (was :class:`ValueError`) and is raised by
:meth:`Marker.evaluate` for missing environment keys, where a bare
``KeyError`` was raised before.
"""
class Environment(TypedDict):
"""
A dictionary that represents a Python environment as captured by
:func:`default_environment`. All fields are required.
"""
implementation_name: str
"""The implementation's identifier, e.g. ``'cpython'``."""
implementation_version: str
"""
The implementation's version, e.g. ``'3.13.0a2'`` for CPython 3.13.0a2, or
``'7.3.13'`` for PyPy3.10 v7.3.13.
"""
os_name: str
"""
The value of :py:data:`os.name`. The name of the operating system dependent module
imported, e.g. ``'posix'``.
"""
platform_machine: str
"""
Returns the machine type, e.g. ``'i386'``.
An empty string if the value cannot be determined.
"""
platform_release: str
"""
The system's release, e.g. ``'2.2.0'`` or ``'NT'``.
An empty string if the value cannot be determined.
"""
platform_system: str
"""
The system/OS name, e.g. ``'Linux'``, ``'Windows'`` or ``'Java'``.
An empty string if the value cannot be determined.
"""
platform_version: str
"""
The system's release version, e.g. ``'#3 on degas'``.
An empty string if the value cannot be determined.
"""
python_full_version: str
"""
The Python version as string ``'major.minor.patchlevel'``.
Note that unlike the Python :py:data:`sys.version`, this value will always include
the patchlevel (it defaults to 0).
"""
platform_python_implementation: str
"""
A string identifying the Python implementation, e.g. ``'CPython'``.
"""
python_version: str
"""The Python version as string ``'major.minor'``."""
sys_platform: str
"""
This string contains a platform identifier that can be used to append
platform-specific components to :py:data:`sys.path`, for instance.
For Unix systems, except on Linux and AIX, this is the lowercased OS name as
returned by ``uname -s`` with the first part of the version as returned by
``uname -r`` appended, e.g. ``'sunos5'`` or ``'freebsd8'``, at the time when Python
was built.
"""
def _normalize_extras(
result: MarkerList | MarkerAtom | str,
) -> MarkerList | MarkerAtom | str:
if isinstance(result, list):
return [_normalize_extras(r) for r in result]
if not isinstance(result, tuple):
return result
lhs, op, rhs = result
if isinstance(lhs, Variable) and lhs.value == "extra" and isinstance(rhs, Value):
normalized_extra = canonicalize_name(rhs.value)
rhs = Value(normalized_extra)
elif isinstance(rhs, Variable) and rhs.value == "extra" and isinstance(lhs, Value):
normalized_extra = canonicalize_name(lhs.value)
lhs = Value(normalized_extra)
elif (
isinstance(rhs, Variable)
and rhs.value in MARKERS_ALLOWING_SET
and isinstance(lhs, Value)
):
# PEP 685 (extras) / PEP 735 (dependency_groups): the set-valued membership
# literal must also be normalized. evaluate() already canonicalizes both
# operands for these keys (see _normalize), so normalizing the literal at
# parse time keeps __str__/__eq__/__hash__ consistent with evaluate() -- e.g.
# Marker('"Foo" in extras') and Marker('"foo" in extras') must compare and
# hash equal (the membership variable is always the right-hand operand).
lhs = Value(canonicalize_name(lhs.value))
return lhs, op, rhs
def _normalize_extra_values(results: MarkerList) -> MarkerList:
"""
Normalize extra values.
"""
return [_normalize_extras(r) for r in results]
def _format_marker(
marker: list[str] | MarkerAtom | str, first: bool | None = True
) -> str:
assert isinstance(marker, (list, tuple, str))
# Unwrap a redundant [[...]] wrapper, but keep the nesting context so a
# nested group keeps the parentheses its and/or precedence needs.
if (
isinstance(marker, list)
and len(marker) == 1
and isinstance(marker[0], (list, tuple))
):
return _format_marker(marker[0], first=first)
if isinstance(marker, list):
inner = (_format_marker(m, first=False) for m in marker)
if first:
return " ".join(inner)
else:
return "(" + " ".join(inner) + ")"
elif isinstance(marker, tuple):
return " ".join([m.serialize() for m in marker])
else:
return marker
_operators: dict[str, Operator] = {
"in": lambda lhs, rhs: lhs in rhs,
"not in": lambda lhs, rhs: lhs not in rhs,
"<": lambda _lhs, _rhs: False,
"<=": operator.eq,
"==": operator.eq,
"!=": operator.ne,
">=": operator.eq,
">": lambda _lhs, _rhs: False,
}
def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool:
op_str = op.serialize()
if key in MARKERS_REQUIRING_VERSION:
try:
spec = Specifier(f"{op_str}{rhs}")
except InvalidSpecifier:
pass
else:
return spec.contains(lhs, prereleases=True)
oper: Operator | None = _operators.get(op_str)
if oper is None:
raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
return oper(lhs, rhs)
def _normalize(
lhs: str, rhs: str | AbstractSet[str], key: str
) -> tuple[str, str | AbstractSet[str]]:
# PEP 685 - Comparison of extra names for optional distribution dependencies
# https://peps.python.org/pep-0685/
# > When comparing extra names, tools MUST normalize the names being
# > compared using the semantics outlined in PEP 503 for names
if key == "extra":
assert isinstance(rhs, str), "extra value must be a string"
# Both sides are normalized at this point already
return (lhs, rhs)
if key in MARKERS_ALLOWING_SET:
if isinstance(rhs, str): # pragma: no cover
return (canonicalize_name(lhs), canonicalize_name(rhs))
else:
return (canonicalize_name(lhs), {canonicalize_name(v) for v in rhs})
# other environment markers don't have such standards
return lhs, rhs
def _lookup_environment(
environment: dict[str, str | AbstractSet[str]], key: str
) -> str | AbstractSet[str]:
try:
return environment[key]
except KeyError:
raise UndefinedEnvironmentName(key) from None
def _evaluate_markers(
markers: MarkerList, environment: dict[str, str | AbstractSet[str]]
) -> bool:
groups: list[list[bool]] = [[]]
for marker in markers:
if isinstance(marker, list):
groups[-1].append(_evaluate_markers(marker, environment))
elif isinstance(marker, tuple):
lhs, op, rhs = marker
if isinstance(lhs, Variable):
environment_key = lhs.value
lhs_value = _lookup_environment(environment, environment_key)
rhs_value = rhs.value
else:
lhs_value = lhs.value
environment_key = rhs.value
rhs_value = _lookup_environment(environment, environment_key)
if not isinstance(lhs_value, str):
raise UndefinedComparison(
f"Set-valued marker {environment_key!r} can only be used "
f'with the membership form (e.g. "<name>" in '
f"{environment_key}); it cannot appear on the left-hand "
f"side of {op.serialize()!r}."
)
lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key)
groups[-1].append(_eval_op(lhs_value, op, rhs_value, key=environment_key))
elif marker == "or":
groups.append([])
elif marker == "and":
pass
else: # pragma: nocover
raise TypeError(f"Unexpected marker {marker!r}")
return any(all(item) for item in groups)
def _format_full_version(info: sys._version_info) -> str:
version = f"{info.major}.{info.minor}.{info.micro}"
kind = info.releaselevel
if kind != "final":
version += kind[0] + str(info.serial)
return version
@functools.cache
def _cached_default_environment() -> Environment:
"""Build the default marker environment for the current Python process.
The values are derived from process-constant data (the running interpreter
and the host platform), so this is cached and built only once. The result is
shared between callers and must never be mutated; :func:`default_environment`
returns a fresh copy.
"""
iver = _format_full_version(sys.implementation.version)
implementation_name = sys.implementation.name
return {
"implementation_name": implementation_name,
"implementation_version": iver,
"os_name": os.name,
"platform_machine": platform.machine(),
"platform_release": platform.release(),
"platform_system": platform.system(),
"platform_version": platform.version(),
"python_full_version": platform.python_version(),
"platform_python_implementation": platform.python_implementation(),
"python_version": ".".join(platform.python_version_tuple()[:2]),
"sys_platform": sys.platform,
}
def default_environment() -> Environment:
"""Return the default marker environment for the current Python process.
This is the base environment used by :meth:`Marker.evaluate`. A fresh copy
is returned on every call so callers may freely mutate the result; a shallow
copy suffices because all values are immutable strings.
.. versionchanged:: 26.3
The environment is computed once per process and cached, since it is
derived from process-constant data. Patching ``platform``/``sys``/``os``
after the first call has no effect; pass an explicit ``environment`` to
:meth:`Marker.evaluate` to evaluate against different values.
"""
return cast("Environment", dict(_cached_default_environment()))
class Marker:
"""Represents a parsed dependency marker expression.
Marker expressions are parsed according to the
:ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.
:param marker: The string representation of a marker expression.
:raises InvalidMarker: If ``marker`` cannot be parsed.
Instances are safe to serialize with :mod:`pickle`. They use a stable
format so the same pickle can be loaded in future packaging releases.
.. versionchanged:: 26.2
Added a stable pickle format. Pickles created with packaging 26.2+ can
be unpickled with future releases. Backward compatibility with pickles
from packaging < 26.2 is supported but may be removed in a future
release.
"""
__slots__ = ("_markers",)
def __init__(self, marker: str) -> None:
# Note: We create a Marker object without calling this constructor in
# packaging.requirements.Requirement. If any additional logic is
# added here, make sure to mirror/adapt Requirement.
# If this fails and throws an error, the repr still expects _markers to
# be defined.
self._markers: MarkerList = []
try:
self._markers = _normalize_extra_values(_parse_marker(marker))
# The attribute `_markers` can be described in terms of a recursive type:
# MarkerList = List[Union[Tuple[Node, ...], str, MarkerList]]
#
# For example, the following expression:
# python_version > "3.6" or (python_version == "3.6" and os_name == "unix")
#
# is parsed into:
# [
# (<Variable('python_version')>, <Op('>')>, <Value('3.6')>),
# 'and',
# [
# (<Variable('python_version')>, <Op('==')>, <Value('3.6')>),
# 'or',
# (<Variable('os_name')>, <Op('==')>, <Value('unix')>)
# ]
# ]
except ParserSyntaxError as e:
raise InvalidMarker(str(e)) from e
@classmethod
def _from_markers(cls, markers: MarkerList) -> Marker:
"""Create a Marker instance from a pre-parsed marker tree.
This avoids re-parsing serialised marker strings when combining markers.
"""
new = cls.__new__(cls)
new._markers = markers
return new
def __str__(self) -> str:
return _format_marker(self._markers)
def __repr__(self) -> str:
return f"<{self.__class__.__name__}({str(self)!r})>"
def __hash__(self) -> int:
return hash(str(self))
def __eq__(self, other: object) -> bool:
if not isinstance(other, Marker):
return NotImplemented
return str(self) == str(other)
def __getstate__(self) -> str:
# Return the marker expression string for compactness and stability.
# Internal Node objects are excluded; the string is re-parsed on load.
return str(self)
def __setstate__(self, state: object) -> None:
if isinstance(state, str):
# New format (26.2+): just the marker expression string.
try:
self._markers = _normalize_extra_values(_parse_marker(state))
except ParserSyntaxError as exc:
raise TypeError(f"Cannot restore Marker from {state!r}") from exc
return
if isinstance(state, dict) and "_markers" in state:
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
markers = state["_markers"]
if isinstance(markers, list):
self._markers = markers
return
if isinstance(state, tuple) and len(state) == 2:
# Old format (packaging <= 26.1, __slots__): (None, {slot: value}).
_, slot_dict = state
if isinstance(slot_dict, dict) and "_markers" in slot_dict:
markers = slot_dict["_markers"]
if isinstance(markers, list):
self._markers = markers
return
raise TypeError(f"Cannot restore Marker from {state!r}")
def __and__(self, other: Marker) -> Marker:
"""Combine this marker with another using ``and``.
.. versionadded:: 26.1
"""
if not isinstance(other, Marker):
return NotImplemented
return self._from_markers([self._markers, "and", other._markers])
def __or__(self, other: Marker) -> Marker:
"""Combine this marker with another using ``or``.
.. versionadded:: 26.1
"""
if not isinstance(other, Marker):
return NotImplemented
return self._from_markers([self._markers, "or", other._markers])
def evaluate(
self,
environment: Mapping[str, str | AbstractSet[str]] | None = None,
context: EvaluateContext = "metadata",
) -> bool:
"""Evaluate a marker.
Return the boolean from evaluating this marker against the environment.
The environment is determined from the current Python process unless
passed in explicitly.
:param environment: Mapping containing keys and values to override the
detected environment.
:param EvaluateContext context: The context in which the marker is
evaluated, which influences what marker names are considered valid.
Accepted values are ``"metadata"`` (for core metadata; default),
``"lock_file"``, and ``"requirement"`` (i.e. all other situations).
:raises UndefinedComparison: If the marker uses a comparison on values
that are not valid versions per the :ref:`specification of version
specifiers <pypug:version-specifiers>`.
:raises UndefinedEnvironmentName: If the marker references a value that
is missing from the evaluation environment.
:returns: ``True`` if the marker matches, otherwise ``False``.
.. versionchanged:: 25.0
Added the ``context`` parameter, which influences which marker names
are considered valid.
"""
current_environment = cast(
"dict[str, str | AbstractSet[str]]", default_environment()
)
if context == "lock_file":
current_environment |= {
"extras": frozenset(),
"dependency_groups": frozenset(),
}
elif context == "metadata":
current_environment["extra"] = ""
if environment is not None:
current_environment |= environment
if "extra" in current_environment:
# The API used to allow setting extra to None. We need to handle
# this case for backwards compatibility. Also skip running
# normalize name if extra is empty.
extra = cast("str | None", current_environment["extra"])
current_environment["extra"] = canonicalize_name(extra) if extra else ""
return _evaluate_markers(
self._markers, _repair_python_full_version(current_environment)
)
def _pep440_python_full_version(python_full_version: str) -> str:
"""
Work around platform.python_version() returning something that is not PEP 440
compliant for non-tagged Python builds.
"""
if python_full_version.endswith("+"):
return f"{python_full_version}local"
return python_full_version
def _repair_python_full_version(
env: dict[str, str | AbstractSet[str]],
) -> dict[str, str | AbstractSet[str]]:
"""
Work around platform.python_version() returning something that is not PEP 440
compliant for non-tagged Python builds.
"""
python_full_version = cast("str", env["python_full_version"])
env["python_full_version"] = _pep440_python_full_version(python_full_version)
return env
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,943 @@
from __future__ import annotations
import dataclasses
import logging
import re
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from datetime import datetime
from typing import (
TYPE_CHECKING,
Any,
Callable,
Protocol,
TypeVar,
cast,
)
from urllib.parse import unquote, urlparse
from .markers import (
Environment,
Marker,
_pep440_python_full_version,
default_environment,
)
from .specifiers import SpecifierSet
from .tags import create_compatible_tags_selector, sys_tags
from .utils import (
NormalizedName,
is_normalized_name,
parse_sdist_filename,
parse_wheel_filename,
)
from .version import Version
if TYPE_CHECKING: # pragma: no cover
from collections.abc import Collection, Iterator
from pathlib import Path
from typing_extensions import Self
from .tags import Tag
_logger = logging.getLogger(__name__)
__all__ = [
"Package",
"PackageArchive",
"PackageDirectory",
"PackageSdist",
"PackageVcs",
"PackageWheel",
"Pylock",
"PylockSelectError",
"PylockUnsupportedVersionError",
"PylockValidationError",
"is_valid_pylock_path",
]
def __dir__() -> list[str]:
return __all__
_T = TypeVar("_T")
_T2 = TypeVar("_T2")
class _FromMappingProtocol(Protocol): # pragma: no cover
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
_FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol)
_PYLOCK_FILE_NAME_RE = re.compile(r"^pylock\.([^.]+)\.toml$")
def is_valid_pylock_path(path: Path) -> bool:
"""Check if the given path is a valid pylock file path."""
return path.name == "pylock.toml" or bool(_PYLOCK_FILE_NAME_RE.match(path.name))
def _toml_key(key: str) -> str:
return key.replace("_", "-")
def _toml_value(key: str, value: Any) -> Any: # noqa: ANN401
if isinstance(value, (Version, Marker, SpecifierSet)):
return str(value)
if isinstance(value, Sequence) and key == "environments":
return [str(v) for v in value]
return value
def _toml_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
return {
_toml_key(key): _toml_value(key, value)
for key, value in data
if value is not None
}
def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None:
"""Get a value from the dictionary and verify it's the expected type."""
if (value := d.get(key)) is None:
return None
if not isinstance(value, expected_type) or (
# Special case: bool is a subclass of int, but TOML distinguishes the two
expected_type is int and isinstance(value, bool)
):
raise PylockValidationError(
f"Unexpected type {type(value).__name__} "
f"(expected {expected_type.__name__})",
context=key,
)
return value
def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T:
"""Get a required value from the dictionary and verify it's the expected type."""
if (value := _get(d, expected_type, key)) is None:
raise _PylockRequiredKeyError(key)
return value
def _get_sequence(
d: Mapping[str, Any], expected_item_type: type[_T], key: str
) -> Sequence[_T] | None:
"""Get a list value from the dictionary and verify it's the expected items type."""
if (value := _get(d, Sequence, key)) is None: # type: ignore[type-abstract]
return None
if isinstance(value, (str, bytes)):
# special case: str and bytes are Sequences, but we want to reject it
raise PylockValidationError(
f"Unexpected type {type(value).__name__} (expected Sequence)",
context=key,
)
for i, item in enumerate(value):
if not isinstance(item, expected_item_type):
raise PylockValidationError(
f"Unexpected type {type(item).__name__} "
f"(expected {expected_item_type.__name__})",
context=f"{key}[{i}]",
)
return value
def _get_as(
d: Mapping[str, Any],
expected_type: type[_T],
target_type: Callable[[_T], _T2],
key: str,
) -> _T2 | None:
"""Get a value from the dictionary, verify it's the expected type,
and convert to the target type.
This assumes the target_type constructor accepts the value.
"""
if (value := _get(d, expected_type, key)) is None:
return None
try:
return target_type(value)
except Exception as e:
raise PylockValidationError(e, context=key) from e
def _get_required_as(
d: Mapping[str, Any],
expected_type: type[_T],
target_type: Callable[[_T], _T2],
key: str,
) -> _T2:
"""Get a required value from the dict, verify it's the expected type,
and convert to the target type."""
if (value := _get_as(d, expected_type, target_type, key)) is None:
raise _PylockRequiredKeyError(key)
return value
def _get_sequence_as(
d: Mapping[str, Any],
expected_item_type: type[_T],
target_item_type: Callable[[_T], _T2],
key: str,
) -> list[_T2] | None:
"""Get list value from dictionary and verify expected items type."""
if (value := _get_sequence(d, expected_item_type, key)) is None:
return None
result = []
try:
for item in value:
typed_item = target_item_type(item)
result.append(typed_item)
except Exception as e:
raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e
return result
def _get_object(
d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str
) -> _FromMappingProtocolT | None:
"""Get a dictionary value from the dictionary and convert it to a dataclass."""
if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract]
return None
try:
return target_type._from_dict(value)
except Exception as e:
raise PylockValidationError(e, context=key) from e
def _get_sequence_of_objects(
d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str
) -> list[_FromMappingProtocolT] | None:
"""Get a list value from the dictionary and convert its items to a dataclass."""
if (value := _get_sequence(d, Mapping, key)) is None: # type: ignore[type-abstract]
return None
result: list[_FromMappingProtocolT] = []
try:
for item in value:
typed_item = target_item_type._from_dict(item)
result.append(typed_item)
except Exception as e:
raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e
return result
def _get_required_sequence_of_objects(
d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str
) -> Sequence[_FromMappingProtocolT]:
"""Get a required list value from the dictionary and convert its items to a
dataclass."""
if (result := _get_sequence_of_objects(d, target_item_type, key)) is None:
raise _PylockRequiredKeyError(key)
return result
def _validate_normalized_name(name: str) -> NormalizedName:
"""Validate that a string is a NormalizedName."""
if not is_normalized_name(name):
raise PylockValidationError(f"Name {name!r} is not normalized")
return NormalizedName(name)
def _validate_path_url(path: str | None, url: str | None) -> None:
if not path and not url:
raise PylockValidationError("path or url must be provided")
def _path_name(path: str | None) -> str | None:
if not path:
return None
# If the path is relative it MAY use POSIX-style path separators explicitly
# for portability
if "/" in path:
return path.rsplit("/", 1)[-1]
elif "\\" in path:
return path.rsplit("\\", 1)[-1]
else:
return path
def _url_name(url: str | None) -> str | None:
if not url:
return None
url_path = urlparse(url).path
# The last path component is percent-encoded, so decode it to the file name
return unquote(url_path.rsplit("/", 1)[-1])
def _validate_hashes(hashes: Mapping[str, Any]) -> Mapping[str, Any]:
if not hashes:
raise PylockValidationError("At least one hash must be provided")
if not all(isinstance(hash_val, str) for hash_val in hashes.values()):
raise PylockValidationError("Hash values must be strings")
return hashes
class PylockValidationError(Exception):
"""Raised when when input data is not spec-compliant."""
context: str | None = None
message: str
def __init__(
self,
cause: str | Exception,
*,
context: str | None = None,
) -> None:
if isinstance(cause, PylockValidationError):
if cause.context:
self.context = (
f"{context}.{cause.context}" if context else cause.context
)
else:
self.context = context
self.message = cause.message
else:
self.context = context
self.message = str(cause)
def __str__(self) -> str:
if self.context:
return f"{self.message} in {self.context!r}"
return self.message
class _PylockRequiredKeyError(PylockValidationError):
def __init__(self, key: str) -> None:
super().__init__("Missing required value", context=key)
class PylockUnsupportedVersionError(PylockValidationError):
"""Raised when encountering an unsupported `lock_version`."""
class PylockSelectError(Exception):
"""Base exception for errors raised by :meth:`Pylock.select`.
.. versionadded:: 26.1
"""
@dataclass(frozen=True, init=False)
class PackageVcs:
type: str
url: str | None = None
path: str | None = None
requested_revision: str | None = None
commit_id: str # type: ignore[misc]
subdirectory: str | None = None
def __init__(
self,
*,
type: str,
url: str | None = None,
path: str | None = None,
requested_revision: str | None = None,
commit_id: str,
subdirectory: str | None = None,
) -> None:
# In Python 3.10+ make dataclass kw_only=True and remove __init__
object.__setattr__(self, "type", type)
object.__setattr__(self, "url", url)
object.__setattr__(self, "path", path)
object.__setattr__(self, "requested_revision", requested_revision)
object.__setattr__(self, "commit_id", commit_id)
object.__setattr__(self, "subdirectory", subdirectory)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
package_vcs = cls(
type=_get_required(d, str, "type"),
url=_get(d, str, "url"),
path=_get(d, str, "path"),
requested_revision=_get(d, str, "requested-revision"),
commit_id=_get_required(d, str, "commit-id"),
subdirectory=_get(d, str, "subdirectory"),
)
_validate_path_url(package_vcs.path, package_vcs.url)
return package_vcs
@dataclass(frozen=True, init=False)
class PackageDirectory:
path: str
editable: bool | None = None
subdirectory: str | None = None
def __init__(
self,
*,
path: str,
editable: bool | None = None,
subdirectory: str | None = None,
) -> None:
# In Python 3.10+ make dataclass kw_only=True and remove __init__
object.__setattr__(self, "path", path)
object.__setattr__(self, "editable", editable)
object.__setattr__(self, "subdirectory", subdirectory)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
return cls(
path=_get_required(d, str, "path"),
editable=_get(d, bool, "editable"),
subdirectory=_get(d, str, "subdirectory"),
)
@dataclass(frozen=True, init=False)
class PackageArchive:
url: str | None = None
path: str | None = None
size: int | None = None
upload_time: datetime | None = None
hashes: Mapping[str, str] # type: ignore[misc]
subdirectory: str | None = None
def __init__(
self,
*,
url: str | None = None,
path: str | None = None,
size: int | None = None,
upload_time: datetime | None = None,
hashes: Mapping[str, str],
subdirectory: str | None = None,
) -> None:
# In Python 3.10+ make dataclass kw_only=True and remove __init__
object.__setattr__(self, "url", url)
object.__setattr__(self, "path", path)
object.__setattr__(self, "size", size)
object.__setattr__(self, "upload_time", upload_time)
object.__setattr__(self, "hashes", hashes)
object.__setattr__(self, "subdirectory", subdirectory)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
package_archive = cls(
url=_get(d, str, "url"),
path=_get(d, str, "path"),
size=_get(d, int, "size"),
upload_time=_get(d, datetime, "upload-time"),
hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
subdirectory=_get(d, str, "subdirectory"),
)
_validate_path_url(package_archive.path, package_archive.url)
return package_archive
@dataclass(frozen=True, init=False)
class PackageSdist:
name: str | None = None
upload_time: datetime | None = None
url: str | None = None
path: str | None = None
size: int | None = None
hashes: Mapping[str, str] # type: ignore[misc]
def __init__(
self,
*,
name: str | None = None,
upload_time: datetime | None = None,
url: str | None = None,
path: str | None = None,
size: int | None = None,
hashes: Mapping[str, str],
) -> None:
# In Python 3.10+ make dataclass kw_only=True and remove __init__
object.__setattr__(self, "name", name)
object.__setattr__(self, "upload_time", upload_time)
object.__setattr__(self, "url", url)
object.__setattr__(self, "path", path)
object.__setattr__(self, "size", size)
object.__setattr__(self, "hashes", hashes)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
package_sdist = cls(
name=_get(d, str, "name"),
upload_time=_get(d, datetime, "upload-time"),
url=_get(d, str, "url"),
path=_get(d, str, "path"),
size=_get(d, int, "size"),
hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
)
_validate_path_url(package_sdist.path, package_sdist.url)
return package_sdist
@property
def filename(self) -> str:
"""Get the filename of the sdist.
.. versionadded:: 26.1
"""
filename = self.name or _path_name(self.path) or _url_name(self.url)
if not filename:
raise PylockValidationError("Cannot determine sdist filename")
return filename
@dataclass(frozen=True, init=False)
class PackageWheel:
name: str | None = None
upload_time: datetime | None = None
url: str | None = None
path: str | None = None
size: int | None = None
hashes: Mapping[str, str] # type: ignore[misc]
def __init__(
self,
*,
name: str | None = None,
upload_time: datetime | None = None,
url: str | None = None,
path: str | None = None,
size: int | None = None,
hashes: Mapping[str, str],
) -> None:
# In Python 3.10+ make dataclass kw_only=True and remove __init__
object.__setattr__(self, "name", name)
object.__setattr__(self, "upload_time", upload_time)
object.__setattr__(self, "url", url)
object.__setattr__(self, "path", path)
object.__setattr__(self, "size", size)
object.__setattr__(self, "hashes", hashes)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
package_wheel = cls(
name=_get(d, str, "name"),
upload_time=_get(d, datetime, "upload-time"),
url=_get(d, str, "url"),
path=_get(d, str, "path"),
size=_get(d, int, "size"),
hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
)
_validate_path_url(package_wheel.path, package_wheel.url)
return package_wheel
@property
def filename(self) -> str:
"""Get the filename of the wheel."""
filename = self.name or _path_name(self.path) or _url_name(self.url)
if not filename:
raise PylockValidationError("Cannot determine wheel filename")
return filename
@dataclass(frozen=True, init=False)
class Package:
name: NormalizedName
version: Version | None = None
marker: Marker | None = None
requires_python: SpecifierSet | None = None
dependencies: Sequence[Mapping[str, Any]] | None = None
vcs: PackageVcs | None = None
directory: PackageDirectory | None = None
archive: PackageArchive | None = None
index: str | None = None
sdist: PackageSdist | None = None
wheels: Sequence[PackageWheel] | None = None
attestation_identities: Sequence[Mapping[str, Any]] | None = None
tool: Mapping[str, Any] | None = None
def __init__(
self,
*,
name: NormalizedName,
version: Version | None = None,
marker: Marker | None = None,
requires_python: SpecifierSet | None = None,
dependencies: Sequence[Mapping[str, Any]] | None = None,
vcs: PackageVcs | None = None,
directory: PackageDirectory | None = None,
archive: PackageArchive | None = None,
index: str | None = None,
sdist: PackageSdist | None = None,
wheels: Sequence[PackageWheel] | None = None,
attestation_identities: Sequence[Mapping[str, Any]] | None = None,
tool: Mapping[str, Any] | None = None,
) -> None:
# In Python 3.10+ make dataclass kw_only=True and remove __init__
object.__setattr__(self, "name", name)
object.__setattr__(self, "version", version)
object.__setattr__(self, "marker", marker)
object.__setattr__(self, "requires_python", requires_python)
object.__setattr__(self, "dependencies", dependencies)
object.__setattr__(self, "vcs", vcs)
object.__setattr__(self, "directory", directory)
object.__setattr__(self, "archive", archive)
object.__setattr__(self, "index", index)
object.__setattr__(self, "sdist", sdist)
object.__setattr__(self, "wheels", wheels)
object.__setattr__(self, "attestation_identities", attestation_identities)
object.__setattr__(self, "tool", tool)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
package = cls(
name=_get_required_as(d, str, _validate_normalized_name, "name"),
version=_get_as(d, str, Version, "version"),
requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
dependencies=_get_sequence(d, Mapping, "dependencies"), # type: ignore[type-abstract]
marker=_get_as(d, str, Marker, "marker"),
vcs=_get_object(d, PackageVcs, "vcs"),
directory=_get_object(d, PackageDirectory, "directory"),
archive=_get_object(d, PackageArchive, "archive"),
index=_get(d, str, "index"),
sdist=_get_object(d, PackageSdist, "sdist"),
wheels=_get_sequence_of_objects(d, PackageWheel, "wheels"),
attestation_identities=_get_sequence(d, Mapping, "attestation-identities"), # type: ignore[type-abstract]
tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]
)
distributions = bool(package.sdist) + len(package.wheels or [])
direct_urls = (
bool(package.vcs) + bool(package.directory) + bool(package.archive)
)
if distributions > 0 and direct_urls > 0:
raise PylockValidationError(
"None of vcs, directory, archive must be set if sdist or wheels are set"
)
if distributions == 0 and direct_urls != 1:
raise PylockValidationError(
"Exactly one of vcs, directory, archive must be set "
"if sdist and wheels are not set"
)
for i, wheel in enumerate(package.wheels or []):
try:
(name, version, _, _) = parse_wheel_filename(wheel.filename)
except Exception as e:
raise PylockValidationError(
f"Invalid wheel filename {wheel.filename!r}",
context=f"wheels[{i}]",
) from e
if name != package.name:
raise PylockValidationError(
f"Name in {wheel.filename!r} is not consistent with "
f"package name {package.name!r}",
context=f"wheels[{i}]",
)
if package.version and version != package.version:
raise PylockValidationError(
f"Version in {wheel.filename!r} is not consistent with "
f"package version {str(package.version)!r}",
context=f"wheels[{i}]",
)
if package.sdist:
try:
name, version = parse_sdist_filename(package.sdist.filename)
except Exception as e:
raise PylockValidationError(
f"Invalid sdist filename {package.sdist.filename!r}",
context="sdist",
) from e
if name != package.name:
raise PylockValidationError(
f"Name in {package.sdist.filename!r} is not consistent with "
f"package name {package.name!r}",
context="sdist",
)
if package.version and version != package.version:
raise PylockValidationError(
f"Version in {package.sdist.filename!r} is not consistent with "
f"package version {str(package.version)!r}",
context="sdist",
)
try:
for i, attestation_identity in enumerate( # noqa: B007
package.attestation_identities or []
):
_get_required(attestation_identity, str, "kind")
except Exception as e:
raise PylockValidationError(
e, context=f"attestation-identities[{i}]"
) from e
return package
@property
def is_direct(self) -> bool:
return not (self.sdist or self.wheels)
@dataclass(frozen=True, init=False)
class Pylock:
"""A class representing a pylock file."""
lock_version: Version
environments: Sequence[Marker] | None = None
requires_python: SpecifierSet | None = None
extras: Sequence[NormalizedName] | None = None
dependency_groups: Sequence[str] | None = None
default_groups: Sequence[str] | None = None
created_by: str # type: ignore[misc]
packages: Sequence[Package] # type: ignore[misc]
tool: Mapping[str, Any] | None = None
def __init__(
self,
*,
lock_version: Version,
environments: Sequence[Marker] | None = None,
requires_python: SpecifierSet | None = None,
extras: Sequence[NormalizedName] | None = None,
dependency_groups: Sequence[str] | None = None,
default_groups: Sequence[str] | None = None,
created_by: str,
packages: Sequence[Package],
tool: Mapping[str, Any] | None = None,
) -> None:
# In Python 3.10+ make dataclass kw_only=True and remove __init__
object.__setattr__(self, "lock_version", lock_version)
object.__setattr__(self, "environments", environments)
object.__setattr__(self, "requires_python", requires_python)
object.__setattr__(self, "extras", extras)
object.__setattr__(self, "dependency_groups", dependency_groups)
object.__setattr__(self, "default_groups", default_groups)
object.__setattr__(self, "created_by", created_by)
object.__setattr__(self, "packages", packages)
object.__setattr__(self, "tool", tool)
@classmethod
def _from_dict(cls, d: Mapping[str, Any]) -> Self:
pylock = cls(
lock_version=_get_required_as(d, str, Version, "lock-version"),
environments=_get_sequence_as(d, str, Marker, "environments"),
extras=_get_sequence_as(d, str, _validate_normalized_name, "extras"),
dependency_groups=_get_sequence(d, str, "dependency-groups"),
default_groups=_get_sequence(d, str, "default-groups"),
created_by=_get_required(d, str, "created-by"),
requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
packages=_get_required_sequence_of_objects(d, Package, "packages"),
tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]
)
if not Version("1") <= pylock.lock_version < Version("2"):
raise PylockUnsupportedVersionError(
f"pylock version {pylock.lock_version} is not supported"
)
if pylock.lock_version > Version("1.0"):
_logger.warning(
"pylock minor version %s is not supported", pylock.lock_version
)
return pylock
@classmethod
def from_dict(cls, d: Mapping[str, Any], /) -> Self:
"""Create and validate a Pylock instance from a TOML dictionary.
Raises :class:`PylockValidationError` if the input data is not
spec-compliant.
"""
return cls._from_dict(d)
def to_dict(self) -> Mapping[str, Any]:
"""Convert the Pylock instance to a TOML dictionary."""
return dataclasses.asdict(self, dict_factory=_toml_dict_factory)
def validate(self) -> None:
"""Validate the Pylock instance against the specification.
Raises :class:`PylockValidationError` otherwise."""
self.from_dict(self.to_dict())
def select(
self,
*,
environment: Environment | None = None,
tags: Sequence[Tag] | None = None,
extras: Collection[str] | None = None,
dependency_groups: Collection[str] | None = None,
prefer_sdist_predicate: Callable[[NormalizedName], bool] | None = None,
) -> Iterator[
tuple[
Package,
PackageVcs
| PackageDirectory
| PackageArchive
| PackageWheel
| PackageSdist,
]
]:
"""Select what to install from the lock file.
The *environment* and *tags* parameters represent the environment being
selected for. If unspecified, ``packaging.markers.default_environment()`` and
``packaging.tags.sys_tags()`` are used.
The *extras* parameter represents the extras to install.
The *dependency_groups* parameter represents the groups to install. If
unspecified, the default groups are used.
The *prefer_sdist_predicate* parameter is called for packages with a source
distribution. If it returns ``True``, the source distribution is selected
before attempting wheel compatibility. If no source distribution is
available, wheel selection proceeds as usual without calling the predicate.
This method must be used on valid Pylock instances (i.e. one obtained
from :meth:`Pylock.from_dict` or if constructed manually, after calling
:meth:`Pylock.validate`).
.. versionadded:: 26.1
.. versionchanged:: 26.3
Added the *prefer_sdist_predicate* parameter.
"""
compatible_tags_selector = create_compatible_tags_selector(
tags if tags is not None else sys_tags()
)
# #. Gather the extras and dependency groups to install and set ``extras`` and
# ``dependency_groups`` for marker evaluation, respectively.
#
# #. ``extras`` SHOULD be set to the empty set by default.
# #. ``dependency_groups`` SHOULD be the set created from
# :ref:`pylock-default-groups` by default.
env = cast(
"dict[str, str | frozenset[str]]",
dict(
environment or {}, # Marker.evaluate will fill-up
extras=frozenset(extras or []),
dependency_groups=frozenset(
(self.default_groups or [])
if dependency_groups is None # to allow selecting no group
else dependency_groups
),
),
)
env_python_full_version = _pep440_python_full_version(
environment["python_full_version"]
if environment
else default_environment()["python_full_version"]
)
# #. Check if the metadata version specified by :ref:`pylock-lock-version` is
# supported; an error or warning MUST be raised as appropriate.
# Covered by lock.validate() which is a precondition for this method.
# #. If :ref:`pylock-requires-python` is specified, check that the environment
# being installed for meets the requirement; an error MUST be raised if it is
# not met.
if self.requires_python and not self.requires_python.contains(
env_python_full_version,
):
raise PylockSelectError(
f"python_full_version {env_python_full_version!r} "
f"in provided environment does not satisfy the Python version "
f"requirement {str(self.requires_python)!r}"
)
# #. If :ref:`pylock-environments` is specified, check that at least one of the
# environment marker expressions is satisfied; an error MUST be raised if no
# expression is satisfied.
if self.environments:
for env_marker in self.environments:
if env_marker.evaluate(
cast("dict[str, str]", environment or {}), context="requirement"
):
break
else:
raise PylockSelectError(
"Provided environment does not satisfy any of the "
"environments specified in the lock file"
)
# #. For each package listed in :ref:`pylock-packages`:
selected_packages_by_name: dict[str, tuple[int, Package]] = {}
for package_index, package in enumerate(self.packages):
# #. If :ref:`pylock-packages-marker` is specified, check if it is
# satisfied;if it isn't, skip to the next package.
if package.marker and not package.marker.evaluate(env, context="lock_file"):
continue
# #. If :ref:`pylock-packages-requires-python` is specified, check if it is
# satisfied; an error MUST be raised if it isn't.
if package.requires_python and not package.requires_python.contains(
env_python_full_version,
):
raise PylockSelectError(
f"python_full_version {env_python_full_version!r} "
f"in provided environment does not satisfy the Python version "
f"requirement {str(package.requires_python)!r} for package "
f"{package.name!r} at packages[{package_index}]"
)
# #. Check that no other conflicting instance of the package has been slated
# to be installed; an error about the ambiguity MUST be raised otherwise.
if package.name in selected_packages_by_name:
raise PylockSelectError(
f"Multiple packages with the name {package.name!r} are "
f"selected at packages[{package_index}] and "
f"packages[{selected_packages_by_name[package.name][0]}]"
)
# #. Check that the source of the package is specified appropriately (i.e.
# there are no conflicting sources in the package entry);
# an error MUST be raised if any issues are found.
# Covered by lock.validate() which is a precondition for this method.
# #. Add the package to the set of packages to install.
selected_packages_by_name[package.name] = (package_index, package)
# #. For each package to be installed:
for package_index, package in selected_packages_by_name.values():
# - If :ref:`pylock-packages-vcs` is set:
if package.vcs is not None:
yield package, package.vcs
# - Else if :ref:`pylock-packages-directory` is set:
elif package.directory is not None:
yield package, package.directory
# - Else if :ref:`pylock-packages-archive` is set:
elif package.archive is not None:
yield package, package.archive
# - Else if source preference selects an available
# :ref:`pylock-packages-sdist`:
elif (
package.sdist is not None
and prefer_sdist_predicate is not None
and prefer_sdist_predicate(package.name)
):
yield package, package.sdist
# - Else if there are entries for :ref:`pylock-packages-wheels`:
elif package.wheels:
# #. Look for the appropriate wheel file based on
# :ref:`pylock-packages-wheels-name`; if one is not found then move
# on to :ref:`pylock-packages-sdist` or an error MUST be raised about
# a lack of source for the project.
best_wheel = next(
compatible_tags_selector(
(wheel, parse_wheel_filename(wheel.filename)[-1])
for wheel in package.wheels
),
None,
)
if best_wheel:
yield package, best_wheel
elif package.sdist is not None:
yield package, package.sdist
else:
raise PylockSelectError(
f"No wheel found matching the provided tags "
f"for package {package.name!r} "
f"at packages[{package_index}], "
f"and no sdist available as a fallback"
)
# - Else if no :ref:`pylock-packages-wheels` file is found or
# :ref:`pylock-packages-sdist` is solely set:
elif package.sdist is not None:
yield package, package.sdist
else:
# Covered by lock.validate() which is a precondition for this method.
raise NotImplementedError # pragma: no cover
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,192 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
from __future__ import annotations
from typing import TYPE_CHECKING
from ._parser import parse_requirement as _parse_requirement
from ._tokenizer import ParserSyntaxError
from .markers import Marker, _normalize_extra_values
from .specifiers import InvalidSpecifier, SpecifierSet
from .utils import canonicalize_name
if TYPE_CHECKING:
from collections.abc import Iterator
__all__ = [
"InvalidRequirement",
"Requirement",
]
def __dir__() -> list[str]:
return __all__
class InvalidRequirement(ValueError):
"""
An invalid requirement was found, users should refer to PEP 508.
.. versionadded:: 16.1
"""
class Requirement:
"""Parse a requirement.
Parse a given requirement string into its parts, such as name, specifier,
URL, and extras. Raises InvalidRequirement on a badly-formed requirement
string.
.. versionadded:: 16.1
.. versionchanged:: 22.0
Added equality (``__eq__``) and hashing (``__hash__``) so requirements
can be compared and stored in sets / dicts.
.. versionchanged:: 23.2
Equality and hashing began canonicalizing requirement names, so
requirements whose names differ only by normalization (e.g.
``Requirement("Foo")`` vs ``Requirement("foo")``) now compare and hash
equal.
Instances are safe to serialize with :mod:`pickle`. They use a stable
format so the same pickle can be loaded in future packaging releases.
.. versionchanged:: 26.2
Added a stable pickle format. Pickles created with packaging 26.2+ can
be unpickled with future releases. Backward compatibility with pickles
from packaging < 26.2 is supported but may be removed in a future
release.
.. versionchanged:: 26.3
The dedicated pickle support introduced in 26.2 did not preserve the
specifier's explicit :attr:`~packaging.specifiers.SpecifierSet.prereleases`
override; it is now included again.
Equality and hashing normalize requirement names, extras, and
equivalent specifiers. The string representation still preserves the
parsed name and extras spelling.
"""
# TODO: Can we test whether something is contained within a requirement?
# If so how do we do that? Do we need to test against the _name_ of
# the thing as well as the version? What about the markers?
# TODO: Can we normalize the name and extra name?
__slots__ = ("extras", "marker", "name", "specifier", "url")
def __init__(self, requirement_string: str) -> None:
try:
parsed = _parse_requirement(requirement_string)
except ParserSyntaxError as e:
raise InvalidRequirement(str(e)) from e
self.name: str = parsed.name
self.url: str | None = parsed.url or None
self.extras: set[str] = set(parsed.extras)
try:
self.specifier: SpecifierSet = SpecifierSet(parsed.specifier)
except InvalidSpecifier as e:
raise InvalidRequirement(str(e)) from e
self.marker: Marker | None = None
if parsed.marker is not None:
self.marker = Marker.__new__(Marker)
self.marker._markers = _normalize_extra_values(parsed.marker)
def _iter_parts(self, name: str) -> Iterator[str]:
yield name
if self.extras:
formatted_extras = ",".join(sorted(self.extras))
yield f"[{formatted_extras}]"
if self.specifier:
yield str(self.specifier)
if self.url:
yield f" @ {self.url}"
if self.marker:
yield " "
if self.marker:
yield f"; {self.marker}"
def __getstate__(self) -> tuple[str, bool | None]:
# Return the requirement string for compactness and stability, paired
# with the specifier's explicit prereleases override, which is not
# captured by the string form. Re-parsed on load to reconstruct all
# other fields.
return (str(self), self.specifier._prereleases)
def __setstate__(self, state: object) -> None:
if isinstance(state, str):
# Format (26.2): just the requirement string.
requirement_string: str = state
prereleases: bool | None = None
elif (
isinstance(state, tuple)
and len(state) == 2
and isinstance(state[0], str)
and (state[1] is None or isinstance(state[1], bool))
):
# New format (26.3+): (requirement string, specifier prereleases).
requirement_string, prereleases = state
elif isinstance(state, dict) and state.keys() >= set(self.__slots__):
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
for key in self.__slots__:
setattr(self, key, state[key])
return
else:
raise TypeError(f"Cannot restore Requirement from {state!r}")
try:
tmp = Requirement(requirement_string)
except InvalidRequirement as exc:
raise TypeError(f"Cannot restore Requirement from {state!r}") from exc
self.name = tmp.name
self.url = tmp.url
self.extras = tmp.extras
self.specifier = tmp.specifier
self.specifier._prereleases = prereleases
self.marker = tmp.marker
def __str__(self) -> str:
return "".join(self._iter_parts(self.name))
def __repr__(self) -> str:
return f"<{self.__class__.__name__}({str(self)!r})>"
def __hash__(self) -> int:
# Mirror __eq__ by hashing the canonical specifier object rather than
# its raw string. ``_iter_parts`` yields ``str(self.specifier)``, which
# is non-canonical, so trailing-zero-equivalent requirements such as
# ``foo==1.0.0`` and ``foo==1.0.0.0`` (which compare equal) would
# otherwise hash differently, breaking the hash/__eq__ invariant.
return hash(
(
canonicalize_name(self.name),
frozenset(canonicalize_name(e) for e in self.extras),
self.specifier,
self.url,
self.marker,
)
)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Requirement):
return NotImplemented
# Extras must be normalized before comparison as per PEP 685.
self_extras = frozenset(canonicalize_name(e) for e in self.extras)
other_extras = frozenset(canonicalize_name(e) for e in other.extras)
return (
canonicalize_name(self.name) == canonicalize_name(other.name)
and self_extras == other_extras
and self.specifier == other.specifier
and self.url == other.url
and self.marker == other.marker
)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,370 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
from __future__ import annotations
import re
from typing import NewType, Union, cast
from .tags import InvalidTag, Tag, UnsortedTagsError, parse_tag
from .version import InvalidVersion, Version, _TrimmedRelease
__all__ = [
"BuildTag",
"InvalidName",
"InvalidSdistFilename",
"InvalidWheelFilename",
"NormalizedName",
"canonicalize_name",
"canonicalize_version",
"is_normalized_name",
"parse_sdist_filename",
"parse_wheel_filename",
]
def __dir__() -> list[str]:
return __all__
BuildTag = Union[tuple[()], tuple[int, str]]
"""
A wheel build tag: an empty tuple, or a ``(build number, build tag suffix)`` pair.
.. versionadded:: 20.9
"""
NormalizedName = NewType("NormalizedName", str)
"""
A :class:`typing.NewType` of :class:`str`, representing a normalized name.
.. versionadded:: 20.4
"""
class InvalidName(ValueError):
"""
An invalid distribution name; users should refer to the packaging user guide.
.. versionadded:: 23.2
"""
class InvalidWheelFilename(ValueError):
"""
An invalid wheel filename was found, users should refer to PEP 427.
.. versionadded:: 20.9
"""
class InvalidSdistFilename(ValueError):
"""
An invalid sdist filename was found, users should refer to the packaging user guide.
.. versionadded:: 20.9
"""
# Core metadata spec for `Name`
_validate_regex = re.compile(
r"[a-z0-9]|[a-z0-9][a-z0-9._-]*[a-z0-9]", re.IGNORECASE | re.ASCII
)
_normalized_regex = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*", re.ASCII)
# PEP 427: The build number must start with a digit.
_build_tag_regex = re.compile(r"(\d+)(.*)", re.ASCII)
# PEP 427: Valid characters for an escaped project name in a wheel filename.
# Requires at least one character so an empty project name is rejected.
_wheel_name_regex = re.compile(r"^[\w._]+\Z", re.UNICODE)
def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName:
"""
This function takes a valid Python package or extra name, and returns the
normalized form of it.
The return type is typed as :class:`NormalizedName`. This allows type
checkers to help require that a string has passed through this function
before use.
If **validate** is true, then the function will check if **name** is a valid
distribution name before normalizing.
:param str name: The name to normalize.
:param bool validate: Check whether the name is a valid distribution name.
:raises InvalidName: If **validate** is true and the name is not an
acceptable distribution name.
>>> from packaging.utils import canonicalize_name
>>> canonicalize_name("Django")
'django'
>>> canonicalize_name("oslo.concurrency")
'oslo-concurrency'
>>> canonicalize_name("requests")
'requests'
.. versionadded:: 16.2
.. versionchanged:: 20.4
The return type was changed to :class:`NormalizedName`.
.. versionchanged:: 23.2
Added the *validate* keyword parameter.
"""
if validate and not _validate_regex.fullmatch(name):
raise InvalidName(f"name is invalid: {name!r}")
# Ensure all ``.`` and ``_`` are ``-``
# Emulates ``re.sub(r"[-_.]+", "-", name).lower()`` from PEP 503
# Much faster than re, and even faster than str.translate
value = name.lower().replace("_", "-").replace(".", "-")
# Condense repeats (faster than regex)
while "--" in value:
value = value.replace("--", "-")
return cast("NormalizedName", value)
def is_normalized_name(name: str) -> bool:
"""
Check if a name is a normalized project name (i.e. a valid name that
:func:`canonicalize_name` would roundtrip to the same value).
The roundtrip only characterizes normalized names for *valid* names. A name
must start and end with an ASCII letter or digit, which
:func:`canonicalize_name` does not enforce: it leaves a leading or trailing
hyphen in place, so such a name roundtrips without being normalized.
:param str name: The name to check.
>>> from packaging.utils import canonicalize_name, is_normalized_name
>>> is_normalized_name("requests")
True
>>> is_normalized_name("Django")
False
>>> canonicalize_name("_not_legal")
'-not-legal'
>>> is_normalized_name("-not-legal") # roundtrips, but not a valid name
False
.. versionadded:: 23.2
"""
return _normalized_regex.fullmatch(name) is not None
def canonicalize_version(
version: Version | str, *, strip_trailing_zero: bool = True
) -> str:
"""Return a canonical form of a version as a string.
This function takes a string representing a package version (or a
:class:`~packaging.version.Version` instance), and returns the
normalized form of it. By default, it strips trailing zeros from
the release segment.
>>> from packaging.utils import canonicalize_version
>>> canonicalize_version('1.0.1')
'1.0.1'
Per PEP 625, versions may have multiple canonical forms, differing
only by trailing zeros.
>>> canonicalize_version('1.0.0')
'1'
>>> canonicalize_version('1.0.0', strip_trailing_zero=False)
'1.0.0'
Invalid versions are returned unaltered.
>>> canonicalize_version('foo bar baz')
'foo bar baz'
>>> canonicalize_version('1.4.0.0.0')
'1.4'
.. versionadded:: 17.1
.. versionchanged:: 21.0
The return type was narrowed to :class:`str`.
.. versionchanged:: 22.0
Added the *strip_trailing_zero* keyword parameter.
"""
if isinstance(version, str):
try:
version = Version(version)
except InvalidVersion:
return str(version)
return str(_TrimmedRelease(version) if strip_trailing_zero else version)
def parse_wheel_filename(
filename: str,
*,
validate_order: bool = False,
) -> tuple[NormalizedName, Version, BuildTag, frozenset[Tag]]:
"""
This function takes the filename of a wheel file, and parses it,
returning a tuple of name, version, build number, and tags.
The name part of the tuple is normalized and typed as
:class:`NormalizedName`. The version portion is an instance of
:class:`~packaging.version.Version`. The build number is ``()`` if
there is no build number in the wheel filename, otherwise a
two-item tuple of an integer for the leading digits and
a string for the rest of the build number. The tags portion is a
frozen set of :class:`~packaging.tags.Tag` instances (as the tag
string format allows multiple tags to be combined into a single
string).
If **validate_order** is true, compressed tag set components are
checked to be in sorted order as required by PEP 425.
:param str filename: The name of the wheel file.
:param bool validate_order: Check whether compressed tag set components
are in sorted order.
:raises InvalidWheelFilename: If the filename in question
does not follow the :ref:`wheel specification
<pypug:binary-distribution-format>`.
>>> from packaging.utils import parse_wheel_filename
>>> from packaging.tags import Tag
>>> from packaging.version import Version
>>> name, ver, build, tags = parse_wheel_filename("foo-1.0-py3-none-any.whl")
>>> name
'foo'
>>> ver == Version('1.0')
True
>>> tags == {Tag("py3", "none", "any")}
True
>>> not build
True
.. versionadded:: 20.9
.. versionchanged:: 23.2
Raises :class:`InvalidWheelFilename` when the version component is invalid.
.. versionadded:: 26.1
The *validate_order* parameter.
.. versionchanged:: 26.3
Raises :class:`InvalidWheelFilename` when an interpreter component is
not an identifier, a tag set component is empty, or the project name is
empty.
"""
if not filename.endswith(".whl"):
raise InvalidWheelFilename(
f"Invalid wheel filename (extension must be '.whl'): {filename!r}"
)
filename = filename[:-4]
dashes = filename.count("-")
if dashes not in (4, 5):
raise InvalidWheelFilename(
f"Invalid wheel filename (wrong number of parts): {filename!r}"
)
parts = filename.split("-", dashes - 2)
name_part = parts[0]
# See PEP 427 for the rules on escaping the project name.
if "__" in name_part or _wheel_name_regex.match(name_part) is None:
raise InvalidWheelFilename(f"Invalid project name: {filename!r}")
name = canonicalize_name(name_part)
try:
version = Version(parts[1])
except InvalidVersion as e:
raise InvalidWheelFilename(
f"Invalid wheel filename (invalid version): {filename!r}"
) from e
if dashes == 5:
build_part = parts[2]
build_match = _build_tag_regex.match(build_part)
if build_match is None:
raise InvalidWheelFilename(
f"Invalid build number: {build_part} in {filename!r}"
)
build = cast("BuildTag", (int(build_match.group(1)), build_match.group(2)))
else:
build = ()
tag_str = parts[-1]
try:
tags = parse_tag(tag_str, validate_order=validate_order)
except UnsortedTagsError:
raise InvalidWheelFilename(
f"Invalid wheel filename (compressed tag set components must be in "
f"sorted order per PEP 425): {filename!r}"
) from None
except InvalidTag:
raise InvalidWheelFilename(
f"Invalid wheel filename (invalid tag component): {filename!r}"
) from None
return (name, version, build, tags)
def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]:
"""
This function takes the filename of a sdist file (as specified
in the `Source distribution format`_ documentation), and parses
it, returning a tuple of the normalized name and version as
represented by an instance of :class:`~packaging.version.Version`.
:param str filename: The name of the sdist file.
:raises InvalidSdistFilename: If the filename does not end
with an sdist extension (``.zip`` or ``.tar.gz``), if it does not
contain a dash separating the name and the version of the distribution,
if the project name is empty, or if the version portion is not a valid
version.
>>> from packaging.utils import parse_sdist_filename
>>> from packaging.version import Version
>>> name, ver = parse_sdist_filename("foo-1.0.tar.gz")
>>> name
'foo'
>>> ver == Version('1.0')
True
.. versionadded:: 20.9
.. versionchanged:: 21.0
Added support for ``.zip`` source distributions.
.. versionchanged:: 23.2
Raises :class:`InvalidSdistFilename` when the version component is invalid.
.. versionchanged:: 26.3
Raises :class:`InvalidSdistFilename` on an empty project name.
.. _Source distribution format: https://packaging.python.org/specifications/source-distribution-format/#source-distribution-file-name
"""
if filename.endswith(".tar.gz"):
file_stem = filename[: -len(".tar.gz")]
elif filename.endswith(".zip"):
file_stem = filename[: -len(".zip")]
else:
raise InvalidSdistFilename(
f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):"
f" {filename!r}"
)
# We are requiring a PEP 440 version, which cannot contain dashes,
# so we split on the last dash.
name_part, sep, version_part = file_stem.rpartition("-")
if not sep:
raise InvalidSdistFilename(f"Invalid sdist filename: {filename!r}")
if not name_part:
raise InvalidSdistFilename(
f"Invalid sdist filename (empty project name): {filename!r}"
)
name = canonicalize_name(name_part)
try:
version = Version(version_part)
except InvalidVersion as e:
raise InvalidSdistFilename(
f"Invalid sdist filename (invalid version): {filename!r}"
) from e
return (name, version)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,760 @@
@Switch01
A_Rog
Aakanksha Agrawal
Abhinav Sagar
ABHYUDAY PRATAP SINGH
abs51295
AceGentile
Adam Chainz
Adam Tse
Adam Wentz
admin
Adrien Morison
ahayrapetyan
Ahilya
AinsworthK
Akash Srivastava
Alan Yee
Albert Tugushev
Albert-Guan
albertg
Alberto Sottile
Aleks Bunin
Ales Erjavec
Alethea Flowers
Alex Gaynor
Alex Grönholm
Alex Hedges
Alex Loosley
Alex Morega
Alex Stachowiak
Alexander Shtyrov
Alexandre Conrad
Alexey Popravka
Aleš Erjavec
Alli
Ami Fischman
Ananya Maiti
Anatoly Techtonik
Anders Kaseorg
Andre Aguiar
Andreas Lutro
Andrei Geacar
Andrew Gaul
Andrew Shymanel
Andrey Bienkowski
Andrey Bulgakov
Andrés Delfino
Andy Freeland
Andy Kluger
Ani Hayrapetyan
Aniruddha Basak
Anish Tambe
Anrs Hu
Anthony Sottile
Antoine Musso
Anton Ovchinnikov
Anton Patrushev
Antonio Alvarado Hernandez
Antony Lee
Antti Kaihola
Anubhav Patel
Anudit Nagar
Anuj Godase
AQNOUCH Mohammed
AraHaan
Arindam Choudhury
Armin Ronacher
Artem
Arun Babu Neelicattu
Ashley Manton
Ashwin Ramaswami
atse
Atsushi Odagiri
Avinash Karhana
Avner Cohen
Awit (Ah-Wit) Ghirmai
Baptiste Mispelon
Barney Gale
barneygale
Bartek Ogryczak
Bastian Venthur
Ben Bodenmiller
Ben Darnell
Ben Hoyt
Ben Mares
Ben Rosser
Bence Nagy
Benjamin Peterson
Benjamin VanEvery
Benoit Pierre
Berker Peksag
Bernard
Bernard Tyers
Bernardo B. Marques
Bernhard M. Wiedemann
Bertil Hatt
Bhavam Vidyarthi
Blazej Michalik
Bogdan Opanchuk
BorisZZZ
Brad Erickson
Bradley Ayers
Brandon L. Reiss
Brandt Bucher
Brett Randall
Brett Rosen
Brian Cristante
Brian Rosner
briantracy
BrownTruck
Bruno Oliveira
Bruno Renié
Bruno S
Bstrdsmkr
Buck Golemon
burrows
Bussonnier Matthias
bwoodsend
c22
Caleb Martinez
Calvin Smith
Carl Meyer
Carlos Liam
Carol Willing
Carter Thayer
Cass
Chandrasekhar Atina
Chih-Hsuan Yen
Chris Brinker
Chris Hunt
Chris Jerdonek
Chris Kuehl
Chris McDonough
Chris Pawley
Chris Pryer
Chris Wolfe
Christian Clauss
Christian Heimes
Christian Oudard
Christoph Reiter
Christopher Hunt
Christopher Snyder
cjc7373
Clark Boylan
Claudio Jolowicz
Clay McClure
Cody
Cody Soyland
Colin Watson
Collin Anderson
Connor Osborn
Cooper Lees
Cooper Ry Lees
Cory Benfield
Cory Wright
Craig Kerstiens
Cristian Sorinel
Cristina
Cristina Muñoz
Curtis Doty
cytolentino
Daan De Meyer
Dale
Damian
Damian Quiroga
Damian Shaw
Dan Black
Dan Savilonis
Dan Sully
Dane Hillard
daniel
Daniel Collins
Daniel Hahler
Daniel Holth
Daniel Jost
Daniel Katz
Daniel Shaulov
Daniele Esposti
Daniele Nicolodi
Daniele Procida
Daniil Konovalenko
Danny Hermes
Danny McClanahan
Darren Kavanagh
Dav Clark
Dave Abrahams
Dave Jones
David Aguilar
David Black
David Bordeynik
David Caro
David D Lowe
David Evans
David Hewitt
David Linke
David Poggi
David Pursehouse
David Runge
David Tucker
David Wales
Davidovich
ddelange
Deepak Sharma
Deepyaman Datta
Denise Yu
dependabot[bot]
derwolfe
Desetude
Devesh Kumar Singh
Diego Caraballo
Diego Ramirez
DiegoCaraballo
Dimitri Merejkowsky
Dimitri Papadopoulos
Dirk Stolle
Dmitry Gladkov
Dmitry Volodin
Domen Kožar
Dominic Davis-Foster
Donald Stufft
Dongweiming
doron zarhi
Dos Moonen
Douglas Thor
DrFeathers
Dustin Ingram
Dwayne Bailey
Ed Morley
Edgar Ramírez
Edgar Ramírez Mondragón
Ee Durbin
Efflam Lemaillet
efflamlemaillet
Eitan Adler
ekristina
elainechan
Eli Schwartz
Elisha Hollander
Ellen Marie Dash
Emil Burzo
Emil Styrke
Emmanuel Arias
Endoh Takanao
enoch
Erdinc Mutlu
Eric Cousineau
Eric Gillingham
Eric Hanchrow
Eric Hopper
Erik M. Bray
Erik Rose
Erwin Janssen
Eugene Vereshchagin
everdimension
Federico
Felipe Peter
Felix Yan
fiber-space
Filip Kokosiński
Filipe Laíns
Finn Womack
finnagin
Flavio Amurrio
Florian Briand
Florian Rathgeber
Francesco
Francesco Montesano
Frost Ming
Gabriel Curio
Gabriel de Perthuis
Garry Polley
gavin
gdanielson
Geoffrey Sneddon
George Song
Georgi Valkov
Georgy Pchelkin
ghost
Giftlin Rajaiah
gizmoguy1
gkdoc
Godefroid Chapelle
Gopinath M
GOTO Hayato
gousaiyang
gpiks
Greg Roodt
Greg Ward
Guilherme Espada
Guillaume Seguin
gutsytechster
Guy Rozendorn
Guy Tuval
gzpan123
Hanjun Kim
Hari Charan
Harsh Vardhan
harupy
Harutaka Kawamura
hauntsaninja
Henrich Hartzer
Henry Schreiner
Herbert Pfennig
Holly Stotelmyer
Honnix
Hsiaoming Yang
Hugo Lopes Tavares
Hugo van Kemenade
Hugues Bruant
Hynek Schlawack
Ian Bicking
Ian Cordasco
Ian Lee
Ian Stapleton Cordasco
Ian Wienand
Igor Kuzmitshov
Igor Sobreira
Ilan Schnell
Illia Volochii
Ilya Baryshev
Inada Naoki
Ionel Cristian Mărieș
Ionel Maries Cristian
Itamar Turner-Trauring
Ivan Pozdeev
J. Nick Koston
Jacob Kim
Jacob Walls
Jaime Sanz
jakirkham
Jakub Kuczys
Jakub Stasiak
Jakub Vysoky
Jakub Wilk
James Cleveland
James Curtin
James Firth
James Gerity
James Polley
Jan Pokorný
Jannis Leidel
Jarek Potiuk
jarondl
Jason Curtis
Jason R. Coombs
JasonMo
JasonMo1
Jay Graves
Jean Abou Samra
Jean-Christophe Fillion-Robin
Jeff Barber
Jeff Dairiki
Jeff Widman
Jelmer Vernooij
jenix21
Jeremy Stanley
Jeremy Zafran
Jesse Rittner
Jiashuo Li
Jim Fisher
Jim Garrison
Jiun Bae
Jivan Amara
Joe Bylund
Joe Michelini
John Paton
John T. Wodder II
John-Scott Atlakson
johnthagen
Jon Banafato
Jon Dufresne
Jon Parise
Jonas Nockert
Jonathan Herbert
Joonatan Partanen
Joost Molenaar
Jorge Niedbalski
Joseph Bylund
Joseph Long
Josh Bronson
Josh Hansen
Josh Schneier
Joshua
Juan Luis Cano Rodríguez
Juanjo Bazán
Judah Rand
Julian Berman
Julian Gethmann
Julien Demoor
Jussi Kukkonen
jwg4
Jyrki Pulliainen
Kai Chen
Kai Mueller
Kamal Bin Mustafa
kasium
kaustav haldar
keanemind
Keith Maxwell
Kelsey Hightower
Kenneth Belitzky
Kenneth Reitz
Kevin Burke
Kevin Carter
Kevin Frommelt
Kevin R Patterson
Kexuan Sun
Kit Randel
Klaas van Schelven
KOLANICH
kpinc
Krishna Oza
Kumar McMillan
Kurt McKee
Kyle Persohn
lakshmanaram
Laszlo Kiss-Kollar
Laurent Bristiel
Laurent LAPORTE
Laurie O
Laurie Opperman
layday
Leon Sasson
Lev Givon
Lincoln de Sousa
Lipis
lorddavidiii
Loren Carvalho
Lucas Cimon
Ludovic Gasc
Lukas Geiger
Lukas Juhrich
Luke Macken
Luo Jiebin
luojiebin
luz.paz
László Kiss Kollár
M00nL1ght
Marc Abramowitz
Marc Tamlyn
Marcus Smith
Mariatta
Mark Kohler
Mark Williams
Markus Hametner
Martey Dodoo
Martin Fischer
Martin Häcker
Martin Pavlasek
Masaki
Masklinn
Matej Stuchlik
Mathew Jennings
Mathieu Bridon
Mathieu Kniewallner
Matt Bacchi
Matt Good
Matt Maker
Matt Robenolt
matthew
Matthew Einhorn
Matthew Feickert
Matthew Gilliard
Matthew Iversen
Matthew Treinish
Matthew Trumbell
Matthew Willson
Matthias Bussonnier
mattip
Maurits van Rees
Max W Chase
Maxim Kurnikov
Maxime Rouyrre
mayeut
mbaluna
mdebi
memoselyk
meowmeowcat
Michael
Michael Aquilina
Michael E. Karpeles
Michael Klich
Michael Mintz
Michael Williamson
michaelpacer
Michał Górny
Mickaël Schoentgen
Miguel Araujo Perez
Mihir Singh
Mike
Mike Hendricks
Min RK
MinRK
Miro Hrončok
Monica Baluna
montefra
Monty Taylor
Muha Ajjan‮
Nadav Wexler
Nahuel Ambrosini
Nate Coraor
Nate Prewitt
Nathan Houghton
Nathaniel J. Smith
Nehal J Wani
Neil Botelho
Nguyễn Gia Phong
Nicholas Serra
Nick Coghlan
Nick Stenning
Nick Timkovich
Nicolas Bock
Nicole Harris
Nikhil Benesch
Nikhil Ladha
Nikita Chepanov
Nikolay Korolev
Nipunn Koorapati
Nitesh Sharma
Niyas Sait
Noah
Noah Gorny
Nowell Strite
NtaleGrey
nvdv
OBITORASU
Ofek Lev
ofrinevo
Oliver Freund
Oliver Jeeves
Oliver Mannion
Oliver Tonnhofer
Olivier Girardot
Olivier Grisel
Ollie Rutherfurd
OMOTO Kenji
Omry Yadan
onlinejudge95
Oren Held
Oscar Benjamin
Oz N Tiram
Pachwenko
Patrick Dubroy
Patrick Jenkins
Patrick Lawson
patricktokeeffe
Patrik Kopkan
Paul Ganssle
Paul Kehrer
Paul Moore
Paul Nasrat
Paul Oswald
Paul van der Linden
Paulus Schoutsen
Pavel Safronov
Pavithra Eswaramoorthy
Pawel Jasinski
Paweł Szramowski
Pekka Klärck
Peter Gessler
Peter Lisák
Peter Waller
petr-tik
Phaneendra Chiruvella
Phil Elson
Phil Freo
Phil Pennock
Phil Whelan
Philip Jägenstedt
Philip Molloy
Philippe Ombredanne
Pi Delport
Pierre-Yves Rofes
Pieter Degroote
pip
Prabakaran Kumaresshan
Prabhjyotsing Surjit Singh Sodhi
Prabhu Marappan
Pradyun Gedam
Prashant Sharma
Pratik Mallya
pre-commit-ci[bot]
Preet Thakkar
Preston Holmes
Przemek Wrzos
Pulkit Goyal
q0w
Qiangning Hong
Qiming Xu
Quentin Lee
Quentin Pradet
R. David Murray
Rafael Caricio
Ralf Schmitt
Razzi Abuissa
rdb
Reece Dunham
Remi Rampin
Rene Dudfield
Riccardo Magliocchetti
Riccardo Schirone
Richard Jones
Richard Si
Ricky Ng-Adam
Rishi
RobberPhex
Robert Collins
Robert McGibbon
Robert Pollak
Robert T. McGibbon
robin elisha robinson
Roey Berman
Rohan Jain
Roman Bogorodskiy
Roman Donchenko
Romuald Brunet
ronaudinho
Ronny Pfannschmidt
Rory McCann
Ross Brattain
Roy Wellington Ⅳ
Ruairidh MacLeod
Russell Keith-Magee
Ryan Shepherd
Ryan Wooden
ryneeverett
Sachi King
Salvatore Rinchiera
sandeepkiran-js
Sander Van Balen
Savio Jomton
schlamar
Scott Kitterman
Sean
seanj
Sebastian Jordan
Sebastian Schaetz
Segev Finer
SeongSoo Cho
Sergey Vasilyev
Seth Michael Larson
Seth Woodworth
Shahar Epstein
Shantanu
shireenrao
Shivansh-007
Shlomi Fish
Shovan Maity
Simeon Visser
Simon Cross
Simon Pichugin
sinoroc
sinscary
snook92
socketubs
Sorin Sbarnea
Srinivas Nyayapati
Stavros Korokithakis
Stefan Scherfke
Stefano Rivera
Stephan Erb
Stephen Rosen
stepshal
Steve (Gadget) Barnes
Steve Barnes
Steve Dower
Steve Kowalik
Steven Myint
Steven Silvester
stonebig
studioj
Stéphane Bidoul
Stéphane Bidoul (ACSONE)
Stéphane Klein
Sumana Harihareswara
Surbhi Sharma
Sviatoslav Sydorenko
Swat009
Sylvain
Takayuki SHIMIZUKAWA
Taneli Hukkinen
tbeswick
Thiago
Thijs Triemstra
Thomas Fenzl
Thomas Grainger
Thomas Guettler
Thomas Johansson
Thomas Kluyver
Thomas Smith
Thomas VINCENT
Tim D. Smith
Tim Gates
Tim Harder
Tim Heap
tim smith
tinruufu
Tobias Hermann
Tom Forbes
Tom Freudenheim
Tom V
Tomas Hrnciar
Tomas Orsava
Tomer Chachamu
Tommi Enenkel | AnB
Tomáš Hrnčiar
Tony Beswick
Tony Narlock
Tony Zhaocheng Tan
TonyBeswick
toonarmycaptain
Toshio Kuratomi
toxinu
Travis Swicegood
Tushar Sadhwani
Tzu-ping Chung
Valentin Haenel
Victor Stinner
victorvpaulo
Vikram - Google
Viktor Szépe
Ville Skyttä
Vinay Sajip
Vincent Philippon
Vinicyus Macedo
Vipul Kumar
Vitaly Babiy
Vladimir Fokow
Vladimir Rutsky
W. Trevor King
Wil Tan
Wilfred Hughes
William Edwards
William ML Leslie
William T Olson
William Woodruff
Wilson Mo
wim glenn
Winson Luk
Wolfgang Maier
Wu Zhenyu
XAMES3
Xavier Fernandez
xoviat
xtreak
YAMAMOTO Takashi
Yen Chi Hsuan
Yeray Diaz Diaz
Yoval P
Yu Jian
Yuan Jing Vincent Yan
Yusuke Hayashi
Zearin
Zhiping Deng
ziebam
Zvezdan Petkovic
Łukasz Langa
Роман Донченко
Семён Марьясин
‮rekcäH nitraM‮
@@ -0,0 +1 @@
pip
@@ -0,0 +1,20 @@
Copyright (c) 2008-present The pip developers (see AUTHORS.txt file)
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.
@@ -0,0 +1,88 @@
Metadata-Version: 2.1
Name: pip
Version: 24.0
Summary: The PyPA recommended tool for installing Python packages.
Author-email: The pip developers <distutils-sig@python.org>
License: MIT
Project-URL: Homepage, https://pip.pypa.io/
Project-URL: Documentation, https://pip.pypa.io
Project-URL: Source, https://github.com/pypa/pip
Project-URL: Changelog, https://pip.pypa.io/en/stable/news/
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Topic :: Software Development :: Build Tools
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
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 :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Python: >=3.7
Description-Content-Type: text/x-rst
License-File: LICENSE.txt
License-File: AUTHORS.txt
pip - The Python Package Installer
==================================
.. image:: https://img.shields.io/pypi/v/pip.svg
:target: https://pypi.org/project/pip/
:alt: PyPI
.. image:: https://img.shields.io/pypi/pyversions/pip
:target: https://pypi.org/project/pip
:alt: PyPI - Python Version
.. image:: https://readthedocs.org/projects/pip/badge/?version=latest
:target: https://pip.pypa.io/en/latest
:alt: Documentation
pip is the `package installer`_ for Python. You can use pip to install packages from the `Python Package Index`_ and other indexes.
Please take a look at our documentation for how to install and use pip:
* `Installation`_
* `Usage`_
We release updates regularly, with a new version every 3 months. Find more details in our documentation:
* `Release notes`_
* `Release process`_
If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms:
* `Issue tracking`_
* `Discourse channel`_
* `User IRC`_
If you want to get involved head over to GitHub to get the source code, look at our development documentation and feel free to jump on the developer mailing lists and chat rooms:
* `GitHub page`_
* `Development documentation`_
* `Development IRC`_
Code of Conduct
---------------
Everyone interacting in the pip project's codebases, issue trackers, chat
rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
.. _package installer: https://packaging.python.org/guides/tool-recommendations/
.. _Python Package Index: https://pypi.org
.. _Installation: https://pip.pypa.io/en/stable/installation/
.. _Usage: https://pip.pypa.io/en/stable/
.. _Release notes: https://pip.pypa.io/en/stable/news.html
.. _Release process: https://pip.pypa.io/en/latest/development/release-process/
.. _GitHub page: https://github.com/pypa/pip
.. _Development documentation: https://pip.pypa.io/en/latest/development
.. _Issue tracking: https://github.com/pypa/pip/issues
.. _Discourse channel: https://discuss.python.org/c/packaging
.. _User IRC: https://kiwiirc.com/nextclient/#ircs://irc.libera.chat:+6697/pypa
.. _Development IRC: https://kiwiirc.com/nextclient/#ircs://irc.libera.chat:+6697/pypa-dev
.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More