...
 
Commits (10)
.DS_Store
.db
*.py[co]
__pycache__/
static/
*.xcodeproj/
.idea/
venv/
.pytest_cache/
*/local_settings.py
*/*.egg-info/
*/build/
image: docker:latest
services:
- docker:dind
stages:
- publish
publish_pypi:
stage: publish
image: python:3.6-alpine3.6
tags:
- docker
script:
- pip install twine
- python setup.py sdist
- twine upload --repository-url https://upload.pypi.org/legacy/ dist/*
\ No newline at end of file
Command-line interface for update-tracker.
## Installation
```
pip install uptr --no-cache -U
```
It can be installed in virtualenv, or globally.
## Usage
Commands are organised in groups to manage user, project and package.
All options in the commands listed here have shorter variants. You can see them with `uptr <group> <command> -h` command.
### Registration, activation, login.
To register, run:
```
uptr user register --email <your email> --password <your password>
```
If you prefer to not provide pcredentials in command line, you can use environment variables `EMAIL` and `PASSWORD`.
After you register, we'll send you an email to verify it's really yours. There'll be a command for you to run to activate account.
After your account is activated, you can login with next command:
```
uptr user login --email <your email> --password <your password>
```
You can use environment variables here also.
Your access token will be stored at `~/.uptr.yaml`.
If you'd like to logout, you can run
```
uptr user logout
```
It will erase your token from `~/.uptr.yaml`.
### Projects management
After you logged in, you can start a new project with command:
```
uptr project start --name <your project name>
```
If you want to have spaces in your project name, please enclose it in quotes:
```
uptr project start --name "Some project with spaces in names"
```
To verify it've successfully created, or just to see a list of your projects run:
```
uptr project list
```
Output is tab-separated lines of name, id, and a flag to mark a project is pinned.
#### Pinning a project
Given in most cases you'll have a tracking project per programming project, to minimize arguments required to operate packages, you can set a default project for some directory:
```
uptr project pin --project-id <project id>
```
You can get project id from the second column of `uptr project list` output.
To check if pinning was succesful, you can look at projects list.
### Packages management
First of all, all packages exist in the scope of a project. In case you don't have a pinned project, or want to use not default one, you have to provide its id as:
```
uptr package --project-id <id> <command> <command options>
```
To add package, run:
```
uptr package upsert --distname <distribution name> --master-site <master site> --package-version <package version>
```
If your package is on pypi, dockerhub or github, distibution name is not required.
For example, to track Django:
```
uptr package upsert -m https://pypi.org/project/Django/ -p 1.11.12
```
Next, you'll have mail notifications about outdated packages.
To update package version, run `upsert` again.
```
uptr package upsert -m https://pypi.org/project/Django/ -p 2.0.5
```
To see packages list, run:
```
uptr package list
```
Output is tab-separated lines of name, current version, latest version, master site, id.
If you don't want a package to be tracked anymore, run:
```
uptr package delete --package-id <package id>
```
You can get package id from the last column of `uptr package list` output.
from setuptools import setup
setup(
name='uptr',
version='0.0.2',
description='Command line interface for update tracker.',
url='https://git.teonite.net/mkhliestov/update-tracker-cli',
author='Nikita Khliestov',
author_email='nikita.khliestov@gmail.com',
packages=['update_tracker_client'],
zip_safe=False,
install_requires=[
'confire==0.2.0',
'click==6.7',
'requests==2.18.4',
],
entry_points={
'console_scripts': [
'uptr=update_tracker_client:entry_point'
]
},
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'Topic :: Software Development :: Build Tools',
'Programming Language :: Python :: 3',
],
)
\ No newline at end of file
import click
from .main import settings
from .projects import project
from .packages import package
from .users import user
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
@click.group(context_settings=CONTEXT_SETTINGS)
@click.version_option(None, '--version', '-V', prog_name='UpdateTracker client')
def entry_point():
"""Update tracker client root help text"""
pass
entry_point.add_command(user)
entry_point.add_command(project)
entry_point.add_command(package)
import os
import requests
from confire import Configuration
GLOBAL_CONFIG_FILE = '/etc/uptr.yaml'
USER_CONFIG_FILE = os.path.expanduser('~/.uptr.yaml')
PROJECT_CONFIG_FILE = os.path.abspath('.uptr.yaml')
class ClientConfiguration(Configuration):
CONF_PATHS = [GLOBAL_CONFIG_FILE, USER_CONFIG_FILE, PROJECT_CONFIG_FILE]
token = None
address = 'http://10.11.0.211/'
project_id = 0
settings = ClientConfiguration.load()
session = requests.Session()
if settings.token:
session.headers.update({'AUTHORIZATION': 'Token %s' % settings.token})
import click
import sys
from click import MissingParameter
from .main import settings, session
from .utils import DistUrl
@click.group()
@click.option('--project-id', '-i', type=int, help='Project id, required if no pinned project.')
@click.pass_context
def package(ctx, project_id):
"""Work with packages in project."""
if settings.token is None:
click.echo('No authorization token provided. Please login.')
sys.exit(1)
current_project_id = project_id or settings.project_id
if not current_project_id:
# Emulation of required parameter with possibility of pinning.
raise MissingParameter(param_hint=['--project-id', '-p'], param_type='option',
message='Please provide it or pin the project.')
ctx.obj = {
'project_id': current_project_id,
}
@package.command()
@click.option('--distname', '-d', required=False,
help='the name of distribution. Optional for pypi, github, dockerhub.')
@click.option('--master-site', '-m', required=True, type=DistUrl(),
help='URL of a web-site, where there is a link to distribution files')
@click.option('--package-version', '-p', required=True,
help='your local version number, e.g. the one you have installed on your machine')
@click.pass_context
def upsert(ctx, distname, master_site, package_version, is_generic):
"""Add or update package."""
if is_generic and not distname:
raise click.MissingParameter(message='Distribution name is required for generic sites.',
param_hint=['--distname', '-d'], param_type='option')
url = settings.address + '/api/projects/%s/packages/' % ctx.obj['project_id']
resp = session.post(url, data={
'distname': distname,
'master_site': master_site,
'version': package_version
})
if resp.status_code == 201:
click.echo('CREATED')
elif resp.status_code == 200:
click.echo('UPDATED')
else:
import ipdb; ipdb.set_trace()
click.echo('FAIL')
sys.exit(1)
@package.command()
@click.pass_context
def list(ctx):
"""
List all tracked projects.\n
Format (tab-delimited): name, current version, latest version, master site, id.
"""
url = settings.address + '/api/projects/%s/packages/' % ctx.obj['project_id']
resp = session.get(url)
data = resp.json()
echo_data = []
for item in data:
pac = item['package']
echo_data.append(
'%s\t%s\t%s\t%s\t%s' % (
pac['distname'],
item['version'],
pac['version'],
pac['master_site'],
item['id']
)
)
click.echo('\n'.join(echo_data))
@package.command()
@click.option('--package-id', '-p', required=True, type=int,
help='id of a package, can be obtained from packages list.')
@click.pass_context
def delete(ctx, package_id):
"""Stop tracking a package."""
url = settings.address + '/api/projects/%s/packages/%s' % (ctx.obj['project_id'], package_id)
resp = session.delete(url)
if resp.status_code == 404:
click.echo('NO PACKAGE')
sys.exit(1)
elif resp.status_code == 204:
click.echo('DELETED')
sys.exit(0)
else:
click.echo('ERROR')
sys.exit(1)
import click
import sys
import yaml
from .main import settings, session, PROJECT_CONFIG_FILE
@click.group()
def project():
"""Create, list, pin projects."""
if settings.token is None:
click.echo('No authorization token provided. Please login.')
sys.exit(1)
@project.command()
@click.option('--name', '-n', required=True)
def start(name):
"""Track new project."""
url = settings.address + '/api/projects/'
resp = session.post(url, data={'name': name})
if resp.status_code != 201:
click.get_current_context().fail(message='Failed to create new project')
sys.exit(1)
click.echo(resp.json()['id'])
@project.command()
@click.option('--project-id', '-i', type=int, required=True)
def pin(project_id):
"""Set project as default (in the scope of directory)."""
with open(PROJECT_CONFIG_FILE, 'w') as ofile:
yaml.dump({'project_id': int(project_id)}, ofile)
@project.command()
def list():
"""
List all tracked projects.\n
Format (tab-delimited): name, id, is_pinned. """
url = settings.address + '/api/projects/'
resp = session.get(url).json()
notification_message = []
for p in resp:
is_pinned_mark = 'PINNED' if int(p['id']) == settings.project_id else ''
notification_message.append(
'%s\t%s\t%s' % (p['name'], p['id'], is_pinned_mark)
)
click.echo('\n'.join(notification_message))
\ No newline at end of file
import click
import sys
import yaml
from .main import settings, session, USER_CONFIG_FILE
@click.group()
def user():
"""Register and login here."""
pass
@user.command()
@click.option('--email', '-e', envvar='EMAIL', help='8 symbols minimum.', required=True)
@click.option('--password', '-p', envvar='PASSWORD', required=True)
def register(email, password):
url = settings.address + '/api/auth/register/'
resp = session.post(url, data={
'email': email,
'password': password,
'password_confirm': password,
'username': email,
})
if resp.status_code != 201:
click.echo(resp.json())
sys.exit(1)
else:
click.echo('Please check your email for verification.')
@user.command()
@click.option('--user-uuid', '-u', required=True)
@click.option('--timestamp', '-t', required=True)
@click.option('--signature', '-s', required=True)
def verify(user_uuid, timestamp, signature):
url = settings.address + '/api/auth/verify-registration/'
resp = session.post(url, data={
'user_id': user_uuid,
'timestamp': timestamp,
'signature': signature,
})
if resp.status_code != 200:
click.echo(resp.json())
sys.exit(1)
else:
click.echo(resp.json()['detail'])
@user.command()
@click.option('--email', '-e', envvar='EMAIL', required=True)
@click.option('--password', '-p', envvar='PASSWORD', required=True)
def login(email, password):
url = settings.address + '/api/auth/login/'
resp = session.post(url, data={
'login': email,
'password': password
})
data = resp.json()
# save token to USER_CONFIG_FILE
with open(USER_CONFIG_FILE, 'r') as ifile:
conf = yaml.load(ifile.read()) or {}
conf['token'] = data['token']
with open(USER_CONFIG_FILE, 'w') as ofile:
yaml.dump(conf, ofile)
# Notification for user
click.echo(data['detail'])
@user.command()
def logout():
with open(USER_CONFIG_FILE, 'r') as ifile:
conf = yaml.load(ifile.read()) or {}
conf['token'] = None
with open(USER_CONFIG_FILE, 'w') as ofile:
yaml.dump(conf, ofile)
import re
from urllib.parse import urlsplit
import click
IMPLEMENTED_SITES = ('github.com', 'pypi.org', 'hub.docker.com')
class URL(click.ParamType):
name = 'url'
regex = re.compile(
r'^(?:http)s?://' # http:// or https://
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain...
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
r'(?::\d+)?' # optional port
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
def convert(self, value, param, ctx):
if not self.regex.search(value):
self.fail('invalid URL', param, ctx)
return value
class DistUrl(URL):
def convert(self, value, param, ctx):
value = super().convert(value, param, ctx)
parsed = urlsplit(value)
ctx.params['is_generic'] = parsed.hostname not in IMPLEMENTED_SITES
return value