Initial commit
This commit is contained in:
commit
fcee973a7a
134
.gitignore
vendored
Normal file
134
.gitignore
vendored
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
*.json
|
||||||
|
.vscode
|
||||||
|
.token
|
||||||
|
responses
|
13
downloader/.env.example
Normal file
13
downloader/.env.example
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
APP_PORT=
|
||||||
|
|
||||||
|
MONGO_USER=
|
||||||
|
MONGO_PASS=
|
||||||
|
MONGO_HOST=
|
||||||
|
MONGO_PORT=
|
||||||
|
MONGO_DB=
|
||||||
|
MONGO_COLLECTION=
|
||||||
|
|
||||||
|
STARS_MIN=
|
||||||
|
STARS_MAX=
|
||||||
|
|
||||||
|
GITHUB_TOKEN=
|
117
downloader/app.py
Normal file
117
downloader/app.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from pymongo import MongoClient, DESCENDING, TEXT
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from config import App, Mongo, Parser
|
||||||
|
from collect import split_by_days
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
client = MongoClient(f'mongodb://{Mongo.Username}:{Mongo.Password}@{Mongo.Host}:{Mongo.Port}') # MongoDB connection string
|
||||||
|
|
||||||
|
db = client[Mongo.Database] # Database name
|
||||||
|
|
||||||
|
if Mongo.Collection not in db.list_collection_names():
|
||||||
|
db.create_collection(Mongo.Collection)
|
||||||
|
|
||||||
|
collection = db[Mongo.Collection] # Collection name
|
||||||
|
|
||||||
|
# Specify the field and options for the index
|
||||||
|
stars_index_key = [('stargazers.totalCount', DESCENDING)]
|
||||||
|
repo_name_index_key = [('nameWithOwner')]
|
||||||
|
|
||||||
|
if 'stars' not in collection.index_information():
|
||||||
|
collection.create_index(stars_index_key, name='stars')
|
||||||
|
|
||||||
|
if 'repoName' not in collection.index_information():
|
||||||
|
collection.create_index(repo_name_index_key, unique=True, name='repoName')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def hello():
|
||||||
|
return 'Hello, World!'
|
||||||
|
|
||||||
|
# Create a new repository
|
||||||
|
@app.route('/repositories', methods=['POST'])
|
||||||
|
def create_repository():
|
||||||
|
data = request.get_json()
|
||||||
|
result = collection.insert_one(data)
|
||||||
|
return jsonify({'message': 'Repository created', 'id': str(result.inserted_id)}), 201
|
||||||
|
|
||||||
|
# Read all repositories with pagination
|
||||||
|
@app.route('/repositories', methods=['GET'])
|
||||||
|
def get_all_repositories():
|
||||||
|
page = int(request.args.get('page', 1)) # Get the page number (default: 1)
|
||||||
|
page_size = int(request.args.get('page_size', 10)) # Get the page size (default: 10)
|
||||||
|
|
||||||
|
# Get the start and end values for the stargazers.totalCount range from query parameters
|
||||||
|
start_value = int(request.args.get('start_value', Parser.MinStars))
|
||||||
|
end_value = int(request.args.get('end_value', Parser.MaxStars))
|
||||||
|
|
||||||
|
search_filter = None
|
||||||
|
if start_value and end_value:
|
||||||
|
search_filter = {
|
||||||
|
'stargazers.totalCount': {
|
||||||
|
'$gte': start_value,
|
||||||
|
'$lte': end_value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate the skip value based on the page and page_size
|
||||||
|
skip = (page - 1) * page_size
|
||||||
|
|
||||||
|
# Retrieve repositories with pagination
|
||||||
|
repositories = list(collection.find(search_filter).skip(skip).limit(page_size))
|
||||||
|
|
||||||
|
# Convert ObjectId to string for JSON serialization for each repository
|
||||||
|
for repo in repositories:
|
||||||
|
repo['_id'] = str(repo['_id'])
|
||||||
|
|
||||||
|
return jsonify(repositories), 200
|
||||||
|
|
||||||
|
# Read a specific repository by ID
|
||||||
|
@app.route('/repositories/<repository_id>', methods=['GET'])
|
||||||
|
def get_repository(repository_id):
|
||||||
|
repository = collection.find_one({'_id': repository_id})
|
||||||
|
if repository:
|
||||||
|
return jsonify(repository), 200
|
||||||
|
else:
|
||||||
|
return jsonify({'message': 'Repository not found'}), 404
|
||||||
|
|
||||||
|
# Update a repository by ID
|
||||||
|
@app.route('/repositories/<repository_id>', methods=['PUT'])
|
||||||
|
def update_repository(repository_id):
|
||||||
|
data = request.get_json()
|
||||||
|
result = collection.update_one({'_id': repository_id}, {'$set': data})
|
||||||
|
if result.modified_count > 0:
|
||||||
|
return jsonify({'message': 'Repository updated'}), 200
|
||||||
|
else:
|
||||||
|
return jsonify({'message': 'Repository not found'}), 404
|
||||||
|
|
||||||
|
# Delete a repository by ID
|
||||||
|
@app.route('/repositories/<repository_id>', methods=['DELETE'])
|
||||||
|
def delete_repository(repository_id):
|
||||||
|
result = collection.delete_one({'_id': repository_id})
|
||||||
|
if result.deleted_count > 0:
|
||||||
|
return jsonify({'message': 'Repository deleted'}), 200
|
||||||
|
else:
|
||||||
|
return jsonify({'message': 'Repository not found'}), 404
|
||||||
|
|
||||||
|
# Parse repositories according to min and max stars from env
|
||||||
|
@app.route('/parse', methods=['GET'])
|
||||||
|
def parse():
|
||||||
|
stars_start = Parser.MinStars
|
||||||
|
stars_finish = Parser.MaxStars
|
||||||
|
|
||||||
|
if stars_start is None or stars_finish is None:
|
||||||
|
return jsonify({'message': 'Invalid input. Both stars_start and stars_finish should be numbers.'}), 400
|
||||||
|
|
||||||
|
stars = f'{stars_start}..{stars_finish}'
|
||||||
|
split_by_days(stars, datetime(2007, 1, 1), datetime(2024, 2, 2))
|
||||||
|
try:
|
||||||
|
return jsonify({'message': 'Data parsed'}, 200)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'message': 'Data not parsed', 'stack': str(e)}, 500)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, port=App.Port)
|
||||||
|
|
235
downloader/collect.py
Normal file
235
downloader/collect.py
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
"""Collect data on the most-starred repos using GitHub's GraphQL API."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from config import Parser
|
||||||
|
|
||||||
|
token = Parser.GithubToken
|
||||||
|
|
||||||
|
def query(payload, variables=None):
|
||||||
|
r = requests.post(
|
||||||
|
'https://api.github.com/graphql',
|
||||||
|
headers={'Authorization': f'bearer {token}'},
|
||||||
|
json={"query": payload, "variables": variables or {}}
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
repo_query = '''
|
||||||
|
query popular_repos($start: String, $num: Int!){
|
||||||
|
rateLimit {
|
||||||
|
cost
|
||||||
|
remaining
|
||||||
|
resetAt
|
||||||
|
}
|
||||||
|
search(query: "is:public %s", type: REPOSITORY, first: $num, after: $start) {
|
||||||
|
repositoryCount
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
... on Repository {
|
||||||
|
nameWithOwner
|
||||||
|
createdAt
|
||||||
|
forkCount
|
||||||
|
isFork
|
||||||
|
updatedAt
|
||||||
|
primaryLanguage {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
stargazers {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
watchers {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
count_query = '''
|
||||||
|
query {
|
||||||
|
rateLimit {
|
||||||
|
cost
|
||||||
|
remaining
|
||||||
|
resetAt
|
||||||
|
}
|
||||||
|
search(query: "is:public %s", type: REPOSITORY, first: 1) {
|
||||||
|
repositoryCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
def get_repos(q, cursor, num):
|
||||||
|
return query(repo_query % q, {'start': cursor, 'num': num})['data']
|
||||||
|
|
||||||
|
|
||||||
|
def get_count(q):
|
||||||
|
return query(count_query % q)['data']['search']['repositoryCount']
|
||||||
|
|
||||||
|
|
||||||
|
def scrape(q, out_file):
|
||||||
|
path = f'responses/{out_file}'
|
||||||
|
if os.path.exists(path):
|
||||||
|
print('Skipping', path, 'already exists')
|
||||||
|
return
|
||||||
|
all_repos = []
|
||||||
|
cursor = None
|
||||||
|
print('Creating', path)
|
||||||
|
while True:
|
||||||
|
r = get_repos(q, cursor, 100)
|
||||||
|
search = r['search']
|
||||||
|
pi = search['pageInfo']
|
||||||
|
cursor = pi['endCursor']
|
||||||
|
has_next = pi['hasNextPage']
|
||||||
|
total = search['repositoryCount']
|
||||||
|
if total > 2000:
|
||||||
|
raise ValueError(f'Too many results for {q}: {total}')
|
||||||
|
all_repos += [e['node'] for e in search['edges']]
|
||||||
|
print(r['rateLimit'])
|
||||||
|
print(len(all_repos), ' / ', total, cursor)
|
||||||
|
if not has_next or r['rateLimit']['remaining'] < 10:
|
||||||
|
break
|
||||||
|
#print(all_repos)
|
||||||
|
from pymongo import MongoClient
|
||||||
|
client = MongoClient("mongodb://admin:admin@localhost:27017")
|
||||||
|
db = client['git']
|
||||||
|
collection = db['repos']
|
||||||
|
for repo in all_repos:
|
||||||
|
entity = {}
|
||||||
|
filter_dict = {}
|
||||||
|
|
||||||
|
for key, value in repo.items():
|
||||||
|
if key == "nameWithOwner":
|
||||||
|
filter_dict[key] = value
|
||||||
|
else:
|
||||||
|
entity[key] = value
|
||||||
|
|
||||||
|
collection.update_one(filter_dict, {"$set": entity}, upsert=True)
|
||||||
|
|
||||||
|
with open(path, 'w') as out:
|
||||||
|
print(out)
|
||||||
|
#json.dump(all_repos, out)
|
||||||
|
time.sleep(4)
|
||||||
|
|
||||||
|
|
||||||
|
def scrape_star_range(low, high):
|
||||||
|
"""Scrape a simple star range [low, high]."""
|
||||||
|
out_file = f'repos.stars={low}..{high}.json'
|
||||||
|
q = 'stars:%d..%d' % (low, high)
|
||||||
|
scrape(q, out_file)
|
||||||
|
|
||||||
|
|
||||||
|
def scrape_breaks():
|
||||||
|
breaks = json.load(open('breaks.json'))
|
||||||
|
for hi, lo in zip(breaks[:-1], breaks[1:]):
|
||||||
|
scrape_star_range(lo, hi - 1)
|
||||||
|
|
||||||
|
|
||||||
|
def scrape_star_dates():
|
||||||
|
for stars in range(123, 15, -1):
|
||||||
|
out_file = f'repos.star={stars}.-2015.json'
|
||||||
|
q = 'stars:%d created:<=2015' % stars
|
||||||
|
scrape(q, out_file)
|
||||||
|
|
||||||
|
out_file = f'repos.star={stars}.2016-.json'
|
||||||
|
q = 'stars:%d created:>=2016' % stars
|
||||||
|
scrape(q, out_file)
|
||||||
|
|
||||||
|
|
||||||
|
def query_for_star_years(stars, start, end):
|
||||||
|
q = 'stars:%s' % stars
|
||||||
|
if start == 2010 and end == 2023:
|
||||||
|
return q
|
||||||
|
elif start == 2010:
|
||||||
|
return f'{q} created:<={end}'
|
||||||
|
elif end == 2023:
|
||||||
|
return f'{q} created:>={start}'
|
||||||
|
else:
|
||||||
|
return f'{q} created:{start}..{end}'
|
||||||
|
|
||||||
|
|
||||||
|
def split_interval(a, b):
|
||||||
|
d = int((b - a) / 2)
|
||||||
|
return [(a, a + d), (a + d + 1, b)]
|
||||||
|
|
||||||
|
|
||||||
|
def split_by_year(stars, start, end):
|
||||||
|
if start == 2010 and end == 2023:
|
||||||
|
c = 1001 # we know this will fail.
|
||||||
|
elif start == end:
|
||||||
|
split_by_days(
|
||||||
|
stars,
|
||||||
|
datetime(start, 1, 1),
|
||||||
|
datetime(start, 12, 31)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
q = query_for_star_years(stars, start, end)
|
||||||
|
c = get_count(q)
|
||||||
|
if c <= 1000:
|
||||||
|
out_file = f'repos.star={stars}.{start}-{end}.json'
|
||||||
|
print(f'query: {q}')
|
||||||
|
scrape(q, out_file)
|
||||||
|
else:
|
||||||
|
if start == end:
|
||||||
|
raise ValueError(f'Can\'t split any more for {stars} / {start}')
|
||||||
|
print(f'{stars} {start}..{end} -> {c}, will split')
|
||||||
|
for a, b in split_interval(start, end):
|
||||||
|
split_by_year(stars, a, b)
|
||||||
|
|
||||||
|
|
||||||
|
def split_by_days(stars, day_start, day_end):
|
||||||
|
start_fmt = day_start.strftime('%Y-%m-%d')
|
||||||
|
end_fmt = day_end.strftime('%Y-%m-%d')
|
||||||
|
q = query_for_star_years(stars, start_fmt, end_fmt)
|
||||||
|
c = get_count(q)
|
||||||
|
if c <= 1000:
|
||||||
|
out_file = f'repos.star={stars}.{start_fmt}-{end_fmt}.json'
|
||||||
|
print(f'query: {q}')
|
||||||
|
scrape(q, out_file)
|
||||||
|
else:
|
||||||
|
days = (day_end - day_start).days
|
||||||
|
if days == 0:
|
||||||
|
raise ValueError(f'Can\'t split any more: {stars} / {day_start} .. {day_end}')
|
||||||
|
for a, b in split_interval(0, days):
|
||||||
|
dt_a = day_start + timedelta(days=a)
|
||||||
|
dt_b = day_start + timedelta(days=b)
|
||||||
|
split_by_days(stars, dt_a, dt_b)
|
||||||
|
|
||||||
|
|
||||||
|
def scrape_star_dates_split():
|
||||||
|
#for stars in range(83, 15, -1):
|
||||||
|
for stars in range(40, 15, -1):
|
||||||
|
split_by_year(stars, 2010, 2023)
|
||||||
|
|
||||||
|
|
||||||
|
def scrape_range_days():
|
||||||
|
# Scrape from a low star range up, splitting by creation date (which never changes).
|
||||||
|
# ranges = [(15, 20), (21, 25), (26, 30), (31, 35), (36, 40), (41, 45), (46, 49)]
|
||||||
|
#ranges = [(50, 60), (61, 70), (71, 80), (81, 90), (91, 100)]
|
||||||
|
#ranges = ranges + [(100, 119), (120, 139), (140, 159), (160, 179), (180, 200)]
|
||||||
|
#ranges = ranges + [(201, 225), (226, 250), (251, 300), (301, 400), (401, 500)]
|
||||||
|
#ranges = ranges + [(501, 700), (701, 1000), (1001, 1500), (1501, 5000), (5001, 1_000_000)]
|
||||||
|
ranges = [(1001, 1500), (1501, 5000), (5001, 1_000_000)]
|
||||||
|
for a, b in ranges:
|
||||||
|
stars = f'{a}..{b}'
|
||||||
|
split_by_days(stars, datetime(2007, 1, 1), datetime(2024, 2, 2))
|
||||||
|
|
||||||
|
|
||||||
|
#if __name__ == '__main__':
|
||||||
|
# scrape_breaks()
|
||||||
|
# scrape_star_dates()
|
||||||
|
# scrape_star_dates_split()
|
||||||
|
#scrape_range_days()
|
29
downloader/config.py
Normal file
29
downloader/config.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from utils.libs.decouple import config, UndefinedValueError
|
||||||
|
from utils.utils import str_to_bool
|
||||||
|
from utils.logger import logger
|
||||||
|
|
||||||
|
def set_conf_value(key):
|
||||||
|
try:
|
||||||
|
value = config(key)
|
||||||
|
if value == '':
|
||||||
|
logger.warn(f'WARNING: The variable {key} is an empty string.')
|
||||||
|
return value
|
||||||
|
except (UndefinedValueError):
|
||||||
|
logger.warn(f'WARNING: Please set the variable {key} in the .env file based on .env.example.')
|
||||||
|
return None
|
||||||
|
|
||||||
|
class App:
|
||||||
|
Port=set_conf_value('APP_PORT') or 5000
|
||||||
|
|
||||||
|
class Mongo:
|
||||||
|
Username=set_conf_value('MONGO_USER')
|
||||||
|
Password=set_conf_value('MONGO_PASS')
|
||||||
|
Host=set_conf_value('MONGO_HOST')
|
||||||
|
Port=set_conf_value('MONGO_PORT')
|
||||||
|
Database=set_conf_value('MONGO_DB') or 'git'
|
||||||
|
Collection=set_conf_value('MONGO_COLLECTION') or 'repos'
|
||||||
|
|
||||||
|
class Parser:
|
||||||
|
MinStars=set_conf_value('STARS_MIN')
|
||||||
|
MaxStars=set_conf_value('STARS_MAX')
|
||||||
|
GithubToken=set_conf_value('GITHUB_TOKEN')
|
314
downloader/utils/libs/decouple.py
Normal file
314
downloader/utils/libs/decouple.py
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import string
|
||||||
|
from shlex import shlex
|
||||||
|
from io import open
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
# Useful for very coarse version differentiation.
|
||||||
|
PYVERSION = sys.version_info
|
||||||
|
|
||||||
|
if PYVERSION >= (3, 0, 0):
|
||||||
|
from configparser import ConfigParser
|
||||||
|
text_type = str
|
||||||
|
else:
|
||||||
|
from ConfigParser import SafeConfigParser as ConfigParser
|
||||||
|
text_type = unicode
|
||||||
|
|
||||||
|
if PYVERSION >= (3, 2, 0):
|
||||||
|
def read_config(parser, file): return parser.read_file(file)
|
||||||
|
else:
|
||||||
|
def read_config(parser, file): return parser.readfp(file)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_ENCODING = 'UTF-8'
|
||||||
|
|
||||||
|
|
||||||
|
# Python 3.10 don't have strtobool anymore. So we move it here.
|
||||||
|
TRUE_VALUES = {"y", "yes", "t", "true", "on", "1"}
|
||||||
|
FALSE_VALUES = {"n", "no", "f", "false", "off", "0"}
|
||||||
|
|
||||||
|
def strtobool(value):
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
value = value.lower()
|
||||||
|
|
||||||
|
if value in TRUE_VALUES:
|
||||||
|
return True
|
||||||
|
elif value in FALSE_VALUES:
|
||||||
|
return False
|
||||||
|
|
||||||
|
raise ValueError("Invalid truth value: " + value)
|
||||||
|
|
||||||
|
|
||||||
|
class UndefinedValueError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Undefined(object):
|
||||||
|
"""
|
||||||
|
Class to represent undefined type.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Reference instance to represent undefined values
|
||||||
|
undefined = Undefined()
|
||||||
|
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
"""
|
||||||
|
Handle .env file format used by Foreman.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, repository):
|
||||||
|
self.repository = repository
|
||||||
|
|
||||||
|
def _cast_boolean(self, value):
|
||||||
|
"""
|
||||||
|
Helper to convert config values to boolean as ConfigParser do.
|
||||||
|
"""
|
||||||
|
value = str(value)
|
||||||
|
return bool(value) if value == '' else bool(strtobool(value))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _cast_do_nothing(value):
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get(self, option, default=undefined, cast=undefined):
|
||||||
|
"""
|
||||||
|
Return the value for option or default if defined.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We can't avoid __contains__ because value may be empty.
|
||||||
|
if option in os.environ:
|
||||||
|
value = os.environ[option]
|
||||||
|
elif option in self.repository:
|
||||||
|
value = self.repository[option]
|
||||||
|
else:
|
||||||
|
if isinstance(default, Undefined):
|
||||||
|
raise UndefinedValueError(
|
||||||
|
'{} not found. Declare it as envvar or define a default value.'.format(option))
|
||||||
|
|
||||||
|
value = default
|
||||||
|
|
||||||
|
if isinstance(cast, Undefined):
|
||||||
|
cast = self._cast_do_nothing
|
||||||
|
elif cast is bool:
|
||||||
|
cast = self._cast_boolean
|
||||||
|
|
||||||
|
return cast(value)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Convenient shortcut to get.
|
||||||
|
"""
|
||||||
|
return self.get(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryEmpty(object):
|
||||||
|
def __init__(self, source='', encoding=DEFAULT_ENCODING):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryIni(RepositoryEmpty):
|
||||||
|
"""
|
||||||
|
Retrieves option keys from .ini files.
|
||||||
|
"""
|
||||||
|
SECTION = 'settings'
|
||||||
|
|
||||||
|
def __init__(self, source, encoding=DEFAULT_ENCODING):
|
||||||
|
self.parser = ConfigParser()
|
||||||
|
with open(source, encoding=encoding) as file_:
|
||||||
|
read_config(self.parser, file_)
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return (key in os.environ or
|
||||||
|
self.parser.has_option(self.SECTION, key))
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.parser.get(self.SECTION, key)
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryEnv(RepositoryEmpty):
|
||||||
|
"""
|
||||||
|
Retrieves option keys from .env files with fall back to os.environ.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, source, encoding=DEFAULT_ENCODING):
|
||||||
|
self.data = {}
|
||||||
|
|
||||||
|
with open(source, encoding=encoding) as file_:
|
||||||
|
for line in file_:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith('#') or '=' not in line:
|
||||||
|
continue
|
||||||
|
k, v = line.split('=', 1)
|
||||||
|
k = k.strip()
|
||||||
|
v = v.strip()
|
||||||
|
if len(v) >= 2 and ((v[0] == "'" and v[-1] == "'") or (v[0] == '"' and v[-1] == '"')):
|
||||||
|
v = v[1:-1]
|
||||||
|
self.data[k] = v
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return key in os.environ or key in self.data
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.data[key]
|
||||||
|
|
||||||
|
|
||||||
|
class RepositorySecret(RepositoryEmpty):
|
||||||
|
"""
|
||||||
|
Retrieves option keys from files,
|
||||||
|
where title of file is a key, content of file is a value
|
||||||
|
e.g. Docker swarm secrets
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, source='/run/secrets/'):
|
||||||
|
self.data = {}
|
||||||
|
|
||||||
|
ls = os.listdir(source)
|
||||||
|
for file in ls:
|
||||||
|
with open(os.path.join(source, file), 'r') as f:
|
||||||
|
self.data[file] = f.read()
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return key in os.environ or key in self.data
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.data[key]
|
||||||
|
|
||||||
|
|
||||||
|
class AutoConfig(object):
|
||||||
|
"""
|
||||||
|
Autodetects the config file and type.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
search_path : str, optional
|
||||||
|
Initial search path. If empty, the default search path is the
|
||||||
|
caller's path.
|
||||||
|
|
||||||
|
"""
|
||||||
|
SUPPORTED = OrderedDict([
|
||||||
|
('settings.ini', RepositoryIni),
|
||||||
|
('.env', RepositoryEnv),
|
||||||
|
])
|
||||||
|
|
||||||
|
encoding = DEFAULT_ENCODING
|
||||||
|
|
||||||
|
def __init__(self, search_path=None):
|
||||||
|
self.search_path = search_path
|
||||||
|
self.config = None
|
||||||
|
|
||||||
|
def _find_file(self, path):
|
||||||
|
# look for all files in the current path
|
||||||
|
for configfile in self.SUPPORTED:
|
||||||
|
filename = os.path.join(path, configfile)
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
return filename
|
||||||
|
|
||||||
|
# search the parent
|
||||||
|
parent = os.path.dirname(path)
|
||||||
|
if parent and parent != os.path.abspath(os.sep):
|
||||||
|
return self._find_file(parent)
|
||||||
|
|
||||||
|
# reached root without finding any files.
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def _load(self, path):
|
||||||
|
# Avoid unintended permission errors
|
||||||
|
try:
|
||||||
|
filename = self._find_file(os.path.abspath(path))
|
||||||
|
except Exception:
|
||||||
|
filename = ''
|
||||||
|
Repository = self.SUPPORTED.get(os.path.basename(filename), RepositoryEmpty)
|
||||||
|
|
||||||
|
self.config = Config(Repository(filename, encoding=self.encoding))
|
||||||
|
|
||||||
|
def _caller_path(self):
|
||||||
|
# MAGIC! Get the caller's module path.
|
||||||
|
frame = sys._getframe()
|
||||||
|
path = os.path.dirname(frame.f_back.f_back.f_code.co_filename)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
if not self.config:
|
||||||
|
self._load(self.search_path or self._caller_path())
|
||||||
|
|
||||||
|
return self.config(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# A pré-instantiated AutoConfig to improve decouple's usability
|
||||||
|
# now just import config and start using with no configuration.
|
||||||
|
config = AutoConfig()
|
||||||
|
|
||||||
|
# Helpers
|
||||||
|
|
||||||
|
class Csv(object):
|
||||||
|
"""
|
||||||
|
Produces a csv parser that return a list of transformed elements.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, cast=text_type, delimiter=',', strip=string.whitespace, post_process=list):
|
||||||
|
"""
|
||||||
|
Parameters:
|
||||||
|
cast -- callable that transforms the item just before it's added to the list.
|
||||||
|
delimiter -- string of delimiters chars passed to shlex.
|
||||||
|
strip -- string of non-relevant characters to be passed to str.strip after the split.
|
||||||
|
post_process -- callable to post process all casted values. Default is `list`.
|
||||||
|
"""
|
||||||
|
self.cast = cast
|
||||||
|
self.delimiter = delimiter
|
||||||
|
self.strip = strip
|
||||||
|
self.post_process = post_process
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
"""The actual transformation"""
|
||||||
|
if value is None:
|
||||||
|
return self.post_process()
|
||||||
|
|
||||||
|
def transform(s): return self.cast(s.strip(self.strip))
|
||||||
|
|
||||||
|
splitter = shlex(value, posix=True)
|
||||||
|
splitter.whitespace = self.delimiter
|
||||||
|
splitter.whitespace_split = True
|
||||||
|
|
||||||
|
return self.post_process(transform(s) for s in splitter)
|
||||||
|
|
||||||
|
|
||||||
|
class Choices(object):
|
||||||
|
"""
|
||||||
|
Allows for cast and validation based on a list of choices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, flat=None, cast=text_type, choices=None):
|
||||||
|
"""
|
||||||
|
Parameters:
|
||||||
|
flat -- a flat list of valid choices.
|
||||||
|
cast -- callable that transforms value before validation.
|
||||||
|
choices -- tuple of Django-like choices.
|
||||||
|
"""
|
||||||
|
self.flat = flat or []
|
||||||
|
self.cast = cast
|
||||||
|
self.choices = choices or []
|
||||||
|
|
||||||
|
self._valid_values = []
|
||||||
|
self._valid_values.extend(self.flat)
|
||||||
|
self._valid_values.extend([value for value, _ in self.choices])
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
transform = self.cast(value)
|
||||||
|
if transform not in self._valid_values:
|
||||||
|
raise ValueError((
|
||||||
|
'Value not in list: {!r}; valid values are {!r}'
|
||||||
|
).format(value, self._valid_values))
|
||||||
|
else:
|
||||||
|
return transform
|
264
downloader/utils/libs/loggedRequests.py
Normal file
264
downloader/utils/libs/loggedRequests.py
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
import re
|
||||||
|
import requests
|
||||||
|
from http import HTTPStatus
|
||||||
|
import json as JSON
|
||||||
|
|
||||||
|
from infra.config import BaseUri, Settings
|
||||||
|
from utils.logger import logger, bcolors
|
||||||
|
from utils.singleton import SingletonMeta
|
||||||
|
|
||||||
|
swagger_api_json_endpoint = '/api-json'
|
||||||
|
api_info_urls = {
|
||||||
|
BaseUri.Iguana: 'Iguana',
|
||||||
|
BaseUri.Pyrador: 'Pyrador',
|
||||||
|
BaseUri.Zoo: 'Zoo'
|
||||||
|
}
|
||||||
|
excluded_endpoints = [
|
||||||
|
('POST', BaseUri.Iguana + '/api/v1/test/controller'),
|
||||||
|
('GET', BaseUri.Iguana + '/api/v1/test/controller'),
|
||||||
|
('DELETE', BaseUri.Iguana + '/api/v1/test/controller'),
|
||||||
|
('GET', BaseUri.Iguana + '/api/v1/health'),
|
||||||
|
('GET', BaseUri.Iguana + '/api/v1/metrics'),
|
||||||
|
('GET', BaseUri.Iguana + '/api/v1/settings'),
|
||||||
|
('POST', BaseUri.Iguana + '/api/v1/settings'),
|
||||||
|
('PUT', BaseUri.Iguana + '/api/v1/settings'),
|
||||||
|
('GET', BaseUri.Iguana + '/api/v1/activity'),
|
||||||
|
('GET', BaseUri.Iguana + '/api/v1/activity/{activity_id}'),
|
||||||
|
('POST', BaseUri.Iguana + '/api/v1/doorlock'),
|
||||||
|
('PUT', BaseUri.Iguana + '/api/v1/profile/set-account-number'),
|
||||||
|
('PUT', BaseUri.Iguana + '/api/v1/profile/address'),
|
||||||
|
('PUT', BaseUri.Iguana + '/api/v1/profile/contact'),
|
||||||
|
('POST', BaseUri.Iguana + '/api/v1/profile/set-firebase-token'),
|
||||||
|
('PUT', BaseUri.Iguana + '/api/v1/profile/balance'),
|
||||||
|
('GET', BaseUri.Iguana + '/api/v1/providable-service/{place_id}'),
|
||||||
|
('POST', BaseUri.Iguana + '/api/v1/light-device/toggle'),
|
||||||
|
('GET', BaseUri.Iguana + '/api/v1/light-device/state/{device_id}'),
|
||||||
|
('PUT', BaseUri.Iguana + '/api/v1/user-place/{place_id}'),
|
||||||
|
('GET', BaseUri.Iguana + '/api/v1/user-place/{place_id}/services'),
|
||||||
|
('PUT', BaseUri.Iguana + '/api/v1/user-place/set/status'),
|
||||||
|
('POST', BaseUri.Iguana + '/api/v1/profile/device/to/service'),
|
||||||
|
('DELETE', BaseUri.Iguana + '/api/v1/profile/device/from/service'),
|
||||||
|
('GET', BaseUri.Iguana + '/api/v1/profile/place/{place_id}/service/devices/{device_category}'),
|
||||||
|
('POST', BaseUri.Iguana + '/api/v1/room'),
|
||||||
|
('GET', BaseUri.Iguana + '/api/v1/room/by/place/{parent_id}'),
|
||||||
|
('PUT', BaseUri.Iguana + '/api/v1/room/{id}'),
|
||||||
|
('DELETE', BaseUri.Iguana + '/api/v1/room/{id}'),
|
||||||
|
('GET', BaseUri.Iguana + '/api/v1/device/list/{type}'),
|
||||||
|
('DELETE', BaseUri.Iguana + '/api/v1/user-place/qrcode'),
|
||||||
|
('POST', BaseUri.Iguana + '/api/v1/billing'),
|
||||||
|
('POST', BaseUri.Iguana + '/api/v1/intercom/acceptCall'), # TODO: test it with notifications
|
||||||
|
('POST', BaseUri.Iguana + '/api/v1/upload/avatar'), # TODO: unable to test
|
||||||
|
('POST', BaseUri.Zoo + '/api/v1/notifications/send-notification'),
|
||||||
|
('POST', BaseUri.Zoo + '/api/v1/notifications/send-sms'),
|
||||||
|
('DELETE', BaseUri.Zoo + '/api/v1/place/available_services')
|
||||||
|
]
|
||||||
|
|
||||||
|
class APICoverageTracker(metaclass=SingletonMeta):
|
||||||
|
def __init__(self):
|
||||||
|
self.called_endpoints = {}
|
||||||
|
self.api_info = self.request_api_info(api_info_urls)
|
||||||
|
self.build_called_endpoints()
|
||||||
|
|
||||||
|
def request_api_info(self, urls):
|
||||||
|
api_info = {}
|
||||||
|
|
||||||
|
for url in urls:
|
||||||
|
res = requests.get(url + swagger_api_json_endpoint)
|
||||||
|
api_info[url] = res.json()
|
||||||
|
|
||||||
|
return api_info
|
||||||
|
|
||||||
|
def build_called_endpoints(self):
|
||||||
|
for url, info in self.api_info.items():
|
||||||
|
try:
|
||||||
|
paths = info.get('paths')
|
||||||
|
if not url in self.called_endpoints:
|
||||||
|
self.called_endpoints[url] = {}
|
||||||
|
for path, methods in paths.items():
|
||||||
|
endpoint = url + path
|
||||||
|
self.called_endpoints[url][path] = {}
|
||||||
|
for method, method_info in methods.items():
|
||||||
|
if (method.upper(), endpoint) in excluded_endpoints:
|
||||||
|
continue
|
||||||
|
self.called_endpoints[url][path][method] = 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('Error happened while getting api info:', e)
|
||||||
|
|
||||||
|
def endpoint_is_called(self, called_endpoint, method):
|
||||||
|
if not Settings.EnableCoverageStatistics:
|
||||||
|
return
|
||||||
|
|
||||||
|
for url, paths in self.called_endpoints.items():
|
||||||
|
for path, methods in paths.items():
|
||||||
|
endpoint = url + path
|
||||||
|
pattern = re.sub(r'{.+?}', r'[^/]+', endpoint) + '$'
|
||||||
|
if re.match(pattern, called_endpoint) and method.lower() in methods:
|
||||||
|
self.called_endpoints[url][path][method.lower()] += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
def print_coverage(self):
|
||||||
|
def calculate_coverage_statistics(total_urls, covered_urls):
|
||||||
|
if total_urls == 0:
|
||||||
|
return 0
|
||||||
|
coverage_percentage = int(covered_urls / total_urls * 100)
|
||||||
|
|
||||||
|
if coverage_percentage < 50:
|
||||||
|
color = bcolors.FAIL
|
||||||
|
elif coverage_percentage < 75:
|
||||||
|
color = bcolors.WARNING
|
||||||
|
else:
|
||||||
|
color = bcolors.OKGREEN
|
||||||
|
|
||||||
|
statistics = f'{coverage_percentage}% ({covered_urls} / {total_urls})'
|
||||||
|
return f'{color}{statistics}{bcolors.ENDC}'
|
||||||
|
|
||||||
|
def count_urls(gateway_url):
|
||||||
|
urls_num = 0
|
||||||
|
covered_urls_num = 0
|
||||||
|
|
||||||
|
for url, paths in self.called_endpoints.items():
|
||||||
|
for path, methods in paths.items():
|
||||||
|
endpoint = url + path
|
||||||
|
if gateway_url in endpoint:
|
||||||
|
for method, num_of_calls in methods.items():
|
||||||
|
urls_num += 1
|
||||||
|
if num_of_calls > 0:
|
||||||
|
covered_urls_num += 1
|
||||||
|
else:
|
||||||
|
logger.warn(f'{method.upper()} {endpoint} is not covered')
|
||||||
|
|
||||||
|
return urls_num, covered_urls_num
|
||||||
|
|
||||||
|
if not Settings.EnableCoverageStatistics:
|
||||||
|
return
|
||||||
|
|
||||||
|
urls_num_sum = 0
|
||||||
|
covered_urls_num_sum = 0
|
||||||
|
|
||||||
|
urls_info = \
|
||||||
|
[(gateway_name, count_urls(gateway_url)) \
|
||||||
|
for gateway_url, gateway_name in api_info_urls.items()]
|
||||||
|
|
||||||
|
logger.info('Coverage statistics:')
|
||||||
|
logger.info()
|
||||||
|
|
||||||
|
for gateway_name, (urls_num, covered_urls_num) in urls_info:
|
||||||
|
coverage_statistics = calculate_coverage_statistics(urls_num, covered_urls_num)
|
||||||
|
message = f' {gateway_name}: {coverage_statistics}'
|
||||||
|
logger.info(message)
|
||||||
|
|
||||||
|
urls_num_sum += urls_num
|
||||||
|
covered_urls_num_sum += covered_urls_num
|
||||||
|
|
||||||
|
coverage_statistics = \
|
||||||
|
calculate_coverage_statistics(urls_num_sum, covered_urls_num_sum)
|
||||||
|
logger.info()
|
||||||
|
logger.info(f' Total: {coverage_statistics}\n')
|
||||||
|
|
||||||
|
class Response(requests.Response):
|
||||||
|
def __init__(self, status_code=HTTPStatus.OK):
|
||||||
|
super().__init__()
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
def log_req(method, url, params=None, data=None, json=None, headers=None):
|
||||||
|
logger.verbose(f'============================================================')
|
||||||
|
logger.verbose(f'[REQUEST] {method} {url}')
|
||||||
|
if params:
|
||||||
|
logger.verbose(f'params: {params}')
|
||||||
|
if data:
|
||||||
|
data = JSON.dumps(data, sort_keys=True, indent=4)
|
||||||
|
logger.verbose(f'data: {data}')
|
||||||
|
if json:
|
||||||
|
json = JSON.dumps(json, sort_keys=True, indent=4)
|
||||||
|
logger.verbose(f'json: {json}')
|
||||||
|
if headers:
|
||||||
|
headers = JSON.dumps(headers, sort_keys=True, indent=4)
|
||||||
|
logger.verbose(f'headers: {headers}')
|
||||||
|
|
||||||
|
def log_res(res: requests.Response):
|
||||||
|
req = res.request
|
||||||
|
logger.verbose(f'[RESPONSE] {req.method} {req.url} {res.status_code}')
|
||||||
|
try:
|
||||||
|
json = JSON.dumps(res.json(), sort_keys=True, indent=4).replace('\\"', '"')
|
||||||
|
lines_num = json.count('\n')
|
||||||
|
max_lines_num = Settings.LoggingResponseMaxLinesNum
|
||||||
|
if lines_num <= max_lines_num:
|
||||||
|
logger.verbose(f'json: {json}')
|
||||||
|
else:
|
||||||
|
stats = f'{lines_num}/{max_lines_num}'
|
||||||
|
logger.verbose(f'Maximum number of lines for response exceeded:', stats)
|
||||||
|
except ValueError:
|
||||||
|
logger.verbose('response:', res.content)
|
||||||
|
except Exception as e:
|
||||||
|
logger.verbose(e)
|
||||||
|
|
||||||
|
def request(method, url, headers=None, **kwargs):
|
||||||
|
APICoverageTracker().endpoint_is_called(url, method)
|
||||||
|
log_req(method, url, params=kwargs.get('params'), \
|
||||||
|
data=kwargs.get('data'), json=kwargs.get('json'), headers=headers)
|
||||||
|
res = requests.request(method, url, **kwargs)
|
||||||
|
log_res(res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def get(url, params=None, headers=None, **kwargs):
|
||||||
|
method = 'GET'
|
||||||
|
APICoverageTracker().endpoint_is_called(url, method)
|
||||||
|
log_req(method, url, params=params, \
|
||||||
|
data=kwargs.get('data'), json=kwargs.get('json'), headers=headers)
|
||||||
|
res = requests.get(url, params=params, headers=headers, **kwargs)
|
||||||
|
log_res(res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def options(url, headers=None, **kwargs):
|
||||||
|
method = 'OPTIONS'
|
||||||
|
APICoverageTracker().endpoint_is_called(url, method)
|
||||||
|
log_req(method, url, params=kwargs.get('params'), \
|
||||||
|
data=kwargs.get('data'), json=kwargs.get('json'), headers=headers)
|
||||||
|
res = requests.options(url, headers=headers, **kwargs)
|
||||||
|
log_res(res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def head(url, headers=None, **kwargs):
|
||||||
|
method = 'HEAD'
|
||||||
|
APICoverageTracker().endpoint_is_called(url, method)
|
||||||
|
log_req(method, url, params=kwargs.get('params'), \
|
||||||
|
data=kwargs.get('data'), json=kwargs.get('json'), headers=headers)
|
||||||
|
res = requests.head(url, headers=headers, **kwargs)
|
||||||
|
log_res(res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def post(url, data=None, json=None, headers=None, **kwargs):
|
||||||
|
method = 'POST'
|
||||||
|
APICoverageTracker().endpoint_is_called(url, method)
|
||||||
|
log_req(method, url, params=kwargs.get('params'), \
|
||||||
|
data=data, json=json, headers=headers)
|
||||||
|
res = requests.post(url, data=data, json=json, headers=headers, **kwargs)
|
||||||
|
log_res(res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def put(url, data=None, headers=None, **kwargs):
|
||||||
|
method = 'PUT'
|
||||||
|
APICoverageTracker().endpoint_is_called(url, method)
|
||||||
|
log_req(method, url, params=kwargs.get('params'), \
|
||||||
|
data=data, json=kwargs.get('json'), headers=headers),
|
||||||
|
res = requests.put(url, data=data, headers=headers, **kwargs)
|
||||||
|
log_res(res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def patch(url, data=None, headers=None, **kwargs):
|
||||||
|
method = 'PATCH'
|
||||||
|
APICoverageTracker().endpoint_is_called(url, method)
|
||||||
|
log_req(method, url, params=kwargs.get('params'), \
|
||||||
|
data=data, json=kwargs.get('json'), headers=headers)
|
||||||
|
res = requests.patch(url, data=data, headers=headers, **kwargs)
|
||||||
|
log_res(res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def delete(url, headers=None, **kwargs):
|
||||||
|
method = 'DELETE'
|
||||||
|
APICoverageTracker().endpoint_is_called(url, method)
|
||||||
|
log_req(method, url, params=kwargs.get('params'), \
|
||||||
|
data=kwargs.get('data'), json=kwargs.get('json'), headers=headers)
|
||||||
|
res = requests.delete(url, headers=headers, **kwargs)
|
||||||
|
log_res(res)
|
||||||
|
return res
|
69
downloader/utils/logger.py
Normal file
69
downloader/utils/logger.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import inspect
|
||||||
|
import traceback
|
||||||
|
from utils.utils import trace_origin
|
||||||
|
|
||||||
|
class bcolors:
|
||||||
|
HEADER = '\033[95m'
|
||||||
|
OKBLUE = '\033[94m'
|
||||||
|
OKCYAN = '\033[96m'
|
||||||
|
OKGREEN = '\033[92m'
|
||||||
|
WARNING = '\033[93m'
|
||||||
|
FAIL = '\033[91m'
|
||||||
|
ENDC = '\033[0m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
UNDERLINE = '\033[4m'
|
||||||
|
|
||||||
|
class logger:
|
||||||
|
def debug(*args, **kwargs):
|
||||||
|
from infra.config import Settings
|
||||||
|
if not Settings.EnableDebugMessages:
|
||||||
|
return
|
||||||
|
print(bcolors.HEADER, end='[DEBUG] ')
|
||||||
|
print(*args, **kwargs)
|
||||||
|
print(bcolors.ENDC, end='')
|
||||||
|
|
||||||
|
def error(*args, **kwargs):
|
||||||
|
print(bcolors.FAIL, end='[ERROR] ')
|
||||||
|
print(*args, **kwargs)
|
||||||
|
print(bcolors.ENDC, end='')
|
||||||
|
|
||||||
|
def warn(*args, trace_origin_flag=False, stacklevel=0, \
|
||||||
|
print_stack=False, **kwargs):
|
||||||
|
|
||||||
|
print(bcolors.WARNING, end='[WARNING] ')
|
||||||
|
if trace_origin_flag:
|
||||||
|
trace_origin(inspect.currentframe(), stacklevel)
|
||||||
|
if print_stack:
|
||||||
|
print(traceback.format_exc())
|
||||||
|
print(*args, **kwargs)
|
||||||
|
print(bcolors.ENDC, end='')
|
||||||
|
|
||||||
|
def verbose(*args, trace_origin_flag=False, stacklevel=0, **kwargs):
|
||||||
|
from infra.config import Settings
|
||||||
|
if not Settings.EnableVerboseMessages:
|
||||||
|
return
|
||||||
|
if trace_origin_flag:
|
||||||
|
trace_origin(inspect.currentframe(), stacklevel)
|
||||||
|
print(bcolors.OKCYAN, end='[VERBOSE] ')
|
||||||
|
print(*args, **kwargs)
|
||||||
|
print(bcolors.ENDC, end='')
|
||||||
|
|
||||||
|
def log(*args, **kwargs):
|
||||||
|
print(bcolors.OKGREEN, end='[LOG] ')
|
||||||
|
print(*args, **kwargs)
|
||||||
|
print(bcolors.ENDC, end='')
|
||||||
|
|
||||||
|
def info(*args, **kwargs):
|
||||||
|
print(bcolors.OKBLUE, end='[INFO] ')
|
||||||
|
print(*args, **kwargs)
|
||||||
|
print(bcolors.ENDC, end='')
|
||||||
|
|
||||||
|
class DisableVerbose(object):
|
||||||
|
def __enter__(self):
|
||||||
|
from infra.config import Settings
|
||||||
|
self.verbose_flag = Settings.EnableVerboseMessages
|
||||||
|
Settings.EnableVerboseMessages = False
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
from infra.config import Settings
|
||||||
|
Settings.EnableVerboseMessages = self.verbose_flag
|
19
downloader/utils/utils.py
Normal file
19
downloader/utils/utils.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
def str_to_bool(val: str):
|
||||||
|
if not val:
|
||||||
|
return False
|
||||||
|
val = val.lower()
|
||||||
|
if val in ('y', 'yes', 't', 'true', 'on', '1'):
|
||||||
|
return True
|
||||||
|
elif val in ('', 'n', 'no', 'f', 'false', 'off', '0'):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise ValueError('invalid truth value %r' % (val,))
|
||||||
|
|
||||||
|
def trace_origin(initial_frame, stacklevel=0):
|
||||||
|
frame = initial_frame.f_back
|
||||||
|
for _ in range(stacklevel + 1):
|
||||||
|
frame = frame.f_back
|
||||||
|
file_name = frame.f_code.co_filename
|
||||||
|
line_number = frame.f_lineno
|
||||||
|
func_name = frame.f_code.co_name
|
||||||
|
print(file_name, ":", line_number, ": ", func_name, ": ")
|
20
mongo/docker-compose.yml
Normal file
20
mongo/docker-compose.yml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
version: '3.6'
|
||||||
|
services:
|
||||||
|
mongodb:
|
||||||
|
image: mongo:4.2.8
|
||||||
|
container_name: mongo
|
||||||
|
ports:
|
||||||
|
- 27017:27017
|
||||||
|
environment:
|
||||||
|
- MONGO_INITDB_DATABASE=test
|
||||||
|
- MONGO_INITDB_ROOT_USERNAME=admin
|
||||||
|
- MONGO_INITDB_ROOT_PASSWORD=admin
|
||||||
|
volumes:
|
||||||
|
- ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:ro
|
||||||
|
- mongodb:/data/db
|
||||||
|
- mongoconfig:/data/configdb
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongodb:
|
||||||
|
mongoconfig:
|
||||||
|
|
13
mongo/docker-entrypoint-initdb.d/mongo-init.js
Normal file
13
mongo/docker-entrypoint-initdb.d/mongo-init.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
db.createUser(
|
||||||
|
{
|
||||||
|
user: "git_downloader",
|
||||||
|
pwd: "password",
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
role: "readWrite",
|
||||||
|
db: "git"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
db.createCollection("repos");
|
Loading…
x
Reference in New Issue
Block a user