Repository: anfederico/cryptoview
Branch: master
Commit: b889ae2a22be
Files: 12
Total size: 20.5 KB
Directory structure:
gitextract_6bbm8dlf/
├── Procfile
├── README.md
├── app.py
├── requirements.txt
├── runtime.txt
├── scripts/
│ ├── __init__.py
│ ├── apis.py
│ ├── models.py
│ └── mongio.py
├── static/
│ └── js/
│ └── helpers.js
├── templates/
│ └── index.html
└── updaters.py
================================================
FILE CONTENTS
================================================
================================================
FILE: Procfile
================================================
web: gunicorn app:app --log-file=-
================================================
FILE: README.md
================================================
## Deprecated
This project is no longer maintained.
================================================
FILE: app.py
================================================
from flask import Flask, redirect, url_for, render_template, request
import threading
import signal
import json
import os
import time
# Locals
from scripts import mongio, apis, settings
app = Flask(__name__)
# ======== Tasks =========================================================== #
def refresh_positions(exchanges):
E = []
for exchange in exchanges:
e = getattr(apis, exchange.title())
key = getattr(settings, '{0}_key'.format(exchange))
secret = getattr(settings, '{0}_secret'.format(exchange))
try:
passphrase = getattr(settings, '{0}_passphrase'.format(exchange))
E.append(e(key, secret, passphrase).get_balances())
except AttributeError:
E.append(e(key, secret).get_balances())
return sum(E).positions()
# ======== Routing =========================================================== #
@app.route('/', methods=['GET'])
def index():
if 'positions' not in mongio.db.collection_names():
mongio.db.create_collection('positions')
mongio.save(settings.mongo_portfolio, 'positions', [])
mongio.save(settings.mongo_portfolio, 'equity', {'btc':{}, 'usd':{}})
positions = mongio.load(settings.mongo_portfolio, 'positions')
equity = mongio.load(settings.mongo_portfolio, 'equity')
return render_template('index.html', p=positions, e=equity)
@app.route('/refresh', methods=['POST'])
def refresh():
positions = refresh_positions(settings.exchanges)
mongio.save(settings.mongo_portfolio, 'positions', positions)
positions = mongio.load(settings.mongo_portfolio, 'positions')
return json.dumps({'success': True, 'positions': positions})
# ======== Main ============================================================== #
if __name__ == "__main__":
app.run()
================================================
FILE: requirements.txt
================================================
Flask
gunicorn
pymongo
pandas
requests
datetime
schedule
================================================
FILE: runtime.txt
================================================
python-3.6.0
================================================
FILE: scripts/__init__.py
================================================
================================================
FILE: scripts/apis.py
================================================
import sys
import time
import hmac
import hashlib
import urllib
import requests
import json
import base64
# Local Files
sys.path.append("..")
from scripts import models
class Gemini:
def __init__(self, api_key, api_secret):
self.api_key = api_key
self.api_secret = api_secret
def raw_balances(self):
url = "https://api.gemini.com/v1/balances"
nonce = int(time.time() * 1000)
message_json = json.dumps({"request": "/v1/balances", "nonce": nonce})
message = base64.b64encode(message_json.encode())
signature = hmac.new(self.api_secret.encode(), message, hashlib.sha384).hexdigest()
headers = {'Content-Type': "text/plain",
'Content-Length': "0",
'X-GEMINI-APIKEY': self.api_key,
'X-GEMINI-PAYLOAD': message,
'X-GEMINI-SIGNATURE': signature,
'Cache-Control': "no-cache"}
response = requests.request("POST", url, headers=headers)
return response.json()
def get_balances(self):
E = models.Exchange('Gemini')
raw = self.raw_balances()
for t in raw:
balance = float(t['available'])
if balance > 0:
ticker = t['currency']
try:
price = requests.get('https://api.gemini.com/v1/pubticker/{0}'.format(ticker + 'btc')).json()['last']
E.tokens.append(models.Token(ticker, balance, 'Gemini', price=price))
except KeyError:
E.tokens.append(models.Token(ticker, balance, 'Gemini'))
return E
class Binance:
def __init__(self, api_key, api_secret):
self.api_key = api_key
self.api_secret = api_secret
def raw_balances(self):
session = requests.session()
session.headers.update({'Accept': 'application/json','User-Agent': 'binance/python','X-MBX-APIKEY': self.api_key})
kwargs = {'params': {'timestamp': int(time.time()*1000)}}
query_string = urllib.parse.urlencode(kwargs['params'])
apisign = hmac.new(self.api_secret.encode('utf-8'), query_string.encode('utf-8'), hashlib.sha256).hexdigest()
kwargs['params']['signature'] = apisign
response = getattr(session, 'get')('https://api.binance.com/api/v3/account', timeout=10, **kwargs)
return response.json()['balances']
def get_balances(self):
E = models.Exchange('Binance')
prices = requests.get('https://api.binance.com//api/v1/ticker/allPrices').json()
prices = {i['symbol']: i['price'] for i in prices}
raw = self.raw_balances()
for t in raw:
balance = float(t['free'])
if balance > 0:
ticker = t['asset']
try:
price = prices['{0}BTC'.format(ticker)]
E.tokens.append(models.Token(ticker, balance, 'Binance', price=price))
except KeyError:
E.tokens.append(models.Token(ticker, balance, 'Binance'))
return E
class Bittrex:
def __init__(self, api_key, api_secret):
self.api_key = api_key
self.api_secret = api_secret
def raw_balances(self):
nonce = str(int(time.time()*1000))
query_string = 'https://bittrex.com/api/v1.1/account/getbalances?'
query_string += 'apikey=' + self.api_key + "&nonce=" + nonce + '&'
apisign = hmac.new(self.api_secret.encode(), query_string.encode(), hashlib.sha512).hexdigest()
response = requests.get(query_string, headers={"apisign": apisign})
return response.json()['result']
def get_balances(self):
E = models.Exchange('Bittrex')
prices = requests.get('https://bittrex.com/api/v1.1/public/getmarketsummaries').json()['result']
prices = {i['MarketName']: i['Last'] for i in prices}
raw = self.raw_balances()
for t in raw:
balance = float(t['Available'])
if balance > 0:
ticker = t['Currency']
try:
price = prices['BTC-{0}'.format(ticker)]
E.tokens.append(models.Token(ticker, balance, 'Bittrex', price=price))
except KeyError:
E.tokens.append(models.Token(ticker, balance, 'Bittrex'))
return E
class Poloniex:
def __init__(self, api_key, api_secret):
self.api_key = api_key
self.api_secret = api_secret
def raw_balances(self):
args = {'command': 'returnCompleteBalances'}
args['nonce'] = int(time.time()*1000000)
data = urllib.parse.urlencode(args)
sign = hmac.new(self.api_secret.encode('utf-8'), data.encode('utf-8'), hashlib.sha512)
response = requests.post('https://poloniex.com/tradingApi', data=args,
headers={'Sign': sign.hexdigest(), 'Key': self.api_key}, timeout=10)
return response.json()
def get_balances(self):
E = models.Exchange('Poloniex')
raw = self.raw_balances()
for t in raw:
balance = float(raw[t]['available'])
if balance > 0:
value = float(raw[t]['btcValue'])
ticker = t
try:
E.tokens.append(models.Token(ticker, balance, 'Poloniex', value=value))
except KeyError:
E.tokens.append(models.Token(ticker, balance, 'Poloniex'))
return E
class Gdax:
def __init__(self, api_key, api_secret, api_passphrase):
self.api_key = api_key
self.api_secret = api_secret
self.api_passphrase = api_passphrase
def raw_balances(self):
timestamp = str(time.time())
message = timestamp+'GET/accounts/'
message = message.encode('ascii')
hmac_key = base64.b64decode(self.api_secret)
signature = hmac.new(hmac_key, message, hashlib.sha256)
signature_b64 = base64.b64encode(signature.digest())
response = requests.get('https://api.gdax.com/accounts/', headers={
'Content-Type': 'Application/JSON',
'CB-ACCESS-SIGN': signature_b64,
'CB-ACCESS-TIMESTAMP': timestamp,
'CB-ACCESS-KEY': self.api_key,
'CB-ACCESS-PASSPHRASE': self.api_passphrase})
return response.json()
def get_balances(self):
E = models.Exchange('Gdax')
raw = self.raw_balances()
for t in raw:
balance = float(t['available'])
if balance > 0:
ticker = t['currency']
try:
price = requests.get('https://api.gdax.com/products/{0}-BTC/ticker'.format(ticker)).json()['price']
E.tokens.append(models.Token(ticker, balance, 'Gdax', price=price))
except KeyError:
E.tokens.append(models.Token(ticker, balance, 'Gdax'))
return E
================================================
FILE: scripts/models.py
================================================
import requests
import copy
def get_performances():
data = requests.get('https://api.coinmarketcap.com/v1/ticker/').json()
p = {}
for ticker in data:
t = dict(ticker)
p[t['symbol']] = {'daily': t['percent_change_24h'],
'weekly': t['percent_change_7d']}
return p
class Exchange:
def __init__(self, name):
self.name = name
self.tokens = []
def __str__(self):
string = "Exchange: {0}\n".format(self.name)
for token in self.tokens:
string += "{0} {1} = {2} BTC\n".format(token.balance, token.name, token.value)
return string
def __add__(self, other):
new = copy.deepcopy(self)
new.name += "/"+other.name
for t1 in new.tokens:
for t2 in other.tokens:
if t1 == t2:
t1 += t2
new.tokens += [t for t in other.tokens if t not in new.tokens]
return new
def __radd__(self, other):
if other is int(0):
return self
else:
return self.__add__(other)
def positions(self):
p = get_performances()
self.tokens.sort(key=lambda t: t.value, reverse=True)
total_btc = sum([token.value for token in self.tokens])
positions = []
for token in self.tokens:
# Ignore dust
if token.value < 0.001:
continue
try:
positions.append({'Token' : token.name,
'Balance' : round(token.balance, 2),
'Value' : round(token.value, 5),
'Allocation' : round(100*token.value/total_btc, 3),
'Daily' : p[token.name]['daily'],
'Weekly' : p[token.name]['weekly'],
'Exchanges' : "/".join(token.exchanges)})
except KeyError as e:
positions.append({'Token' : token.name,
'Balance' : round(token.balance, 2),
'Value' : round(token.value, 5),
'Allocation' : round(100*token.value/total_btc, 3),
'Daily' : '--',
'Weekly' : '--',
'Exchanges' : "/".join(token.exchanges)})
return positions
class Token:
def __init__(self, name, balance, exchange, value=None, price=None):
self.name = name.upper()
self.balance = float(balance)
self.exchanges = [exchange]
if name == "BTC":
self.value = float(balance)
return
if name == "USD" or name == "USDT":
self.value = 0
return
try:
self.value = float(value)
except TypeError:
try:
self.value = float(price)*float(balance)
except TypeError:
self.value = 0
def __str__(self):
return "{0} {1} = {2} BTC".format(self.balance, self.name, self.value)
def __eq__(self, other):
return self.name == other.name
def __add__(self, other):
self.balance += other.balance
self.value += other.value
self.exchanges += other.exchanges
return self
================================================
FILE: scripts/mongio.py
================================================
import pymongo
import json
import sys
# Local Files
sys.path.append("..")
from scripts import settings
client = pymongo.MongoClient(settings.mongo_server, settings.mongo_id)
db = client[settings.mongo_client]
db.authenticate(settings.mongo_user, settings.mongo_pass)
def save(account, datatype, data):
d = db.positions.find_one({'account': account})
if d is not None:
d[datatype] = json.dumps(data)
db.positions.save(d)
else:
db.positions.insert_one({'account': account, datatype: json.dumps(data)})
def load(account, datatype):
d = db.positions.find_one({'account': account})
return json.loads(d[datatype])
================================================
FILE: static/js/helpers.js
================================================
var rancol = function () {
return function (bg) {
var hue = Math.floor(Math.random() * 360);
var hsl = 'hsl('+hue+', 90%, 70%)'; // 100 87.5
function checkHex(v) {
return 1 === v.length ? '0'+v : v;
}
var data, r, g, b, a,
cnv = document.createElement('canvas'),
ctx = cnv.getContext('2d'),
alpha = /a\(/.test(hsl),
output = {};
return cnv.width = cnv.height = 1,
bg && (ctx.fillStyle = bg, ctx.fillRect(0, 0, 1, 1)),
ctx.fillStyle = hsl,
ctx.fillRect(0, 0, 1, 1),
data = ctx.getImageData(0, 0, 1, 1).data,
r = data[0],
g = data[1],
b = data[2],
a = (data[3] / 255).toFixed(2),
output.hex = '#'+checkHex(r.toString(16))+checkHex(g.toString(16))+checkHex(b.toString(16)),
output.hex;
};
}();
function colorize(n) {
var colors = []
for (var i = 0; i < n; i++ ){
colors.push(rancol())
}
return colors
}
================================================
FILE: templates/index.html
================================================
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="author" content="Anthony Federico">
<title>cryptoview</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.8/handlebars.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.13/semantic.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.bundle.min.js"></script>
<script src="../static/js/stupidtable.min.js"></script>
<script src="../static/js/helpers.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.10/semantic.css">
</head>
<style>
body {
background-color: #2b2b42;
}
</style>
<body>
<div class="two ui buttons">
<button data-chart="equity-chart" class="ui button charts">Equity Chart</button>
<button data-chart="allocations-chart" class="ui button charts">Portfolio Allocations</button>
</div>
<button id="refresh" class="circular ui icon button" style="position:absolute;left:18;top:50">
<i id="refresh-icon" class="icon refresh"></i>
</button>
<br>
<br>
<div id="container" style="width:75%;margin:auto">
<canvas id="equity-chart" width="300" height="100"></canvas>
</div>
<div id="container" style="width:75%;margin:auto">
<canvas id="allocations-chart" width="300" height="100" style="display:none" ></canvas>
</div>
<br>
<div id="positions"></div>
</body>
</html>
<script>
$(document).on("click", ".charts", function() {
c = $(this).attr('data-chart')
e = document.getElementsByClassName("charts");
for (var i = 0; i < e.length; i++ ){
id = $(e[i]).attr('data-chart')
document.getElementById(id).style.display = "none"
}
document.getElementById(c).style.display = "block"
})
</script>
<script>
var equity = {{ e|tojson }}
var dayEquity = Object.keys(equity['btc'])
var btcEquity = Object.values(equity['btc'])
var usdEquity = Object.values(equity['usd'])
new Chart(document.getElementById("equity-chart"), {
type: 'line',
data: {
labels: dayEquity,
datasets: [{
data: btcEquity,
label: "BTC",
borderColor: "#3e95cd",
fill: false,
yAxisID: "y-axis-1"
}, {
data: usdEquity,
label: "USD",
borderColor: "#c45850",
fill: false,
yAxisID: "y-axis-0"
}
]
},
options: {
legend: {
labels: {
fontColor: 'white'
}
},
scales: {
yAxes: [{
position: "left",
"id": "y-axis-0",
}, {
position: "right",
"id": "y-axis-1"
}]
}
}
});
var allocations = {{ p|tojson }}
var tokenAllocations = []
var valueAllocations = []
for (i = 0; i < allocations.length; i++) {
tokenAllocations.push(allocations[i]['Token'])
valueAllocations.push(allocations[i]['Value'])
}
new Chart(document.getElementById("allocations-chart"), {
type: 'doughnut',
data: {
labels: tokenAllocations,
datasets: [{
labels: tokenAllocations,
backgroundColor: colorize(tokenAllocations.length),
data: valueAllocations,
}
]
},
options: {
legend: {
labels: {
fontColor: 'white'
}
}
}
});
</script>
{% raw %}
<script id="positions-template" type="text/x-handlebars-template">
<table id="sortable" class="ui sortable celled large inverted compact table" style="background-color:#2b2b42">
<thead>
<tr class="center aligned">
<th data-sort="string" style="color:#c04560"><p>Ticker</p></th>
<th data-sort="float" style="color:#c04560"><p>Balance</p></th>
<th data-sort="float" style="color:#c04560"><p>Value</p></th>
<th data-sort="float" style="color:#c04560"><p>Allocation</p></th>
<th data-sort="float" style="color:#c04560"><p>Daily</p></th>
<th data-sort="float" style="color:#c04560"><p>Weekly</p></th>
<th style="color:#c04560"><p>Exchanges</p></th>
</tr>
</thead>
<tbody>
{{#positions}}
<tr class="center aligned">
<td><strong><p>{{Token}}</p></strong></td>
<td><i><p>{{Balance}}</p></i></td>
<td><p>{{Value}}</p></td>
<td><p>{{Allocation}}%</p></td>
<td><p>{{Daily}}%</p></td>
<td><p>{{Weekly}}%</p></td>
<td><p>{{Exchanges}}</p></td>
</tr>
{{/positions}}
</tbody>
</table>
</script>
{% endraw %}
<script>
var source = $("#positions-template").html()
var template = Handlebars.compile(source)
var positions = {'positions': {{ p|tojson }}}
$('#positions').html(template(positions))
$("#sortable").stupidtable()
</script>
<script>
$(document).on("click", "#refresh", function() {
document.getElementById("refresh-icon").classList.add('loading')
$.post({
type: "POST",
url: "/refresh",
data: {},
success: function(response){
var success = JSON.parse(response)['success']
var positions = JSON.parse(response)['positions']
if (success) {
$('#positions').html(template({'positions': positions}))
$("#sortable").stupidtable()
//refreshAllocations(positions)
document.getElementById("refresh-icon").classList.remove('loading')
}
}
})
})
</script>
<script>
var allocations = []
var tokens = []
function refreshAllocations(p) {
var allocations = []
var tokens = []
for (var i = 0; i < p.length; ++i) {
allocations.push(p[i]['Allocation'])
tokens.push(p[i]['Token'])
if (i == p.length) {
chartAllocations.update_values([{values: allocations}], tokens);
}
}
let dataAllocations = {
labels: tokens,
datasets: [
{
title: "Allocations",
values: allocations
}
]
};
let chartAllocations = new Chart({
parent: "#allocations",
data: dataAllocations,
type: 'percentage'
});
}
//refreshAllocations({{ p|tojson }})
</script>
================================================
FILE: updaters.py
================================================
import requests
import datetime
import schedule
import time
# Local Files
from scripts import mongio, settings
def get_btc_price():
btc = requests.get('https://api.coinmarketcap.com/v1/ticker/bitcoin/').json()
return float(btc[0]['price_usd'])
def update_equity():
now = datetime.datetime.now().strftime("%Y-%m-%d")
print('{0} | Updating equity'.format(now))
positions = mongio.load(settings.mongo_portfolio, 'positions')
btc = sum([p['Value'] for p in positions])
usd = btc*get_btc_price()
equity = mongio.load(settings.mongo_portfolio, 'equity')
equity['btc'][now] = round(btc, 3)
equity['usd'][now] = round(usd, 3)
mongio.save(settings.mongo_portfolio, 'equity', equity)
if __name__ == "__main__":
print('Starting scheduler...')
schedule.every(15).minutes.do(update_equity)
while True:
schedule.run_pending()
time.sleep(1)
gitextract_6bbm8dlf/ ├── Procfile ├── README.md ├── app.py ├── requirements.txt ├── runtime.txt ├── scripts/ │ ├── __init__.py │ ├── apis.py │ ├── models.py │ └── mongio.py ├── static/ │ └── js/ │ └── helpers.js ├── templates/ │ └── index.html └── updaters.py
SYMBOL INDEX (41 symbols across 6 files)
FILE: app.py
function refresh_positions (line 15) | def refresh_positions(exchanges):
function index (line 31) | def index():
function refresh (line 42) | def refresh():
FILE: scripts/apis.py
class Gemini (line 15) | class Gemini:
method __init__ (line 16) | def __init__(self, api_key, api_secret):
method raw_balances (line 20) | def raw_balances(self):
method get_balances (line 35) | def get_balances(self):
class Binance (line 50) | class Binance:
method __init__ (line 51) | def __init__(self, api_key, api_secret):
method raw_balances (line 55) | def raw_balances(self):
method get_balances (line 65) | def get_balances(self):
class Bittrex (line 81) | class Bittrex:
method __init__ (line 82) | def __init__(self, api_key, api_secret):
method raw_balances (line 86) | def raw_balances(self):
method get_balances (line 94) | def get_balances(self):
class Poloniex (line 110) | class Poloniex:
method __init__ (line 111) | def __init__(self, api_key, api_secret):
method raw_balances (line 115) | def raw_balances(self):
method get_balances (line 124) | def get_balances(self):
class Gdax (line 138) | class Gdax:
method __init__ (line 139) | def __init__(self, api_key, api_secret, api_passphrase):
method raw_balances (line 144) | def raw_balances(self):
method get_balances (line 159) | def get_balances(self):
FILE: scripts/models.py
function get_performances (line 4) | def get_performances():
class Exchange (line 13) | class Exchange:
method __init__ (line 14) | def __init__(self, name):
method __str__ (line 18) | def __str__(self):
method __add__ (line 24) | def __add__(self, other):
method __radd__ (line 34) | def __radd__(self, other):
method positions (line 40) | def positions(self):
class Token (line 69) | class Token:
method __init__ (line 70) | def __init__(self, name, balance, exchange, value=None, price=None):
method __str__ (line 88) | def __str__(self):
method __eq__ (line 91) | def __eq__(self, other):
method __add__ (line 94) | def __add__(self, other):
FILE: scripts/mongio.py
function save (line 13) | def save(account, datatype, data):
function load (line 21) | def load(account, datatype):
FILE: static/js/helpers.js
function checkHex (line 5) | function checkHex(v) {
function colorize (line 27) | function colorize(n) {
FILE: updaters.py
function get_btc_price (line 9) | def get_btc_price():
function update_equity (line 13) | def update_equity():
Condensed preview — 12 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (22K chars).
[
{
"path": "Procfile",
"chars": 35,
"preview": "web: gunicorn app:app --log-file=-\n"
},
{
"path": "README.md",
"chars": 52,
"preview": "## Deprecated\n\nThis project is no longer maintained."
},
{
"path": "app.py",
"chars": 1806,
"preview": "from flask import Flask, redirect, url_for, render_template, request\n\nimport threading\nimport signal\nimport json\nimport"
},
{
"path": "requirements.txt",
"chars": 57,
"preview": "Flask\ngunicorn\npymongo\npandas\nrequests\ndatetime\nschedule\n"
},
{
"path": "runtime.txt",
"chars": 13,
"preview": "python-3.6.0\n"
},
{
"path": "scripts/__init__.py",
"chars": 1,
"preview": "\n"
},
{
"path": "scripts/apis.py",
"chars": 6940,
"preview": "import sys\nimport time\nimport hmac\nimport hashlib\nimport urllib\nimport requests\nimport json\nimport base64\n\n# Local Files"
},
{
"path": "scripts/models.py",
"chars": 3431,
"preview": "import requests\nimport copy\n\ndef get_performances():\n data = requests.get('https://api.coinmarketcap.com/v1/ticker/')"
},
{
"path": "scripts/mongio.py",
"chars": 656,
"preview": "import pymongo\nimport json\nimport sys\n\n# Local Files\nsys.path.append(\"..\")\nfrom scripts import settings\n\nclient = pymong"
},
{
"path": "static/js/helpers.js",
"chars": 1002,
"preview": "var rancol = function () {\n return function (bg) {\n var hue = Math.floor(Math.random() * 360);\n var hsl"
},
{
"path": "templates/index.html",
"chars": 6051,
"preview": "<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"author\" content=\"Anthony Federico\">\n <title>cryp"
},
{
"path": "updaters.py",
"chars": 902,
"preview": "import requests\nimport datetime\nimport schedule\nimport time\n\n# Local Files\nfrom scripts import mongio, settings\n\ndef get"
}
]
About this extraction
This page contains the full source code of the anfederico/cryptoview GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 12 files (20.5 KB), approximately 5.5k tokens, and a symbol index with 41 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.