plum -> fig
This commit is contained in:
parent
4d35d47969
commit
0cafdc9c6c
21 changed files with 33 additions and 33 deletions
0
fig/cli/__init__.py
Normal file
0
fig/cli/__init__.py
Normal file
41
fig/cli/colors.py
Normal file
41
fig/cli/colors.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
NAMES = [
|
||||
'grey',
|
||||
'red',
|
||||
'green',
|
||||
'yellow',
|
||||
'blue',
|
||||
'magenta',
|
||||
'cyan',
|
||||
'white'
|
||||
]
|
||||
|
||||
|
||||
def get_pairs():
|
||||
for i, name in enumerate(NAMES):
|
||||
yield(name, str(30 + i))
|
||||
yield('intense_' + name, str(30 + i) + ';1')
|
||||
|
||||
|
||||
def ansi(code):
|
||||
return '\033[{0}m'.format(code)
|
||||
|
||||
|
||||
def ansi_color(code, s):
|
||||
return '{0}{1}{2}'.format(ansi(code), s, ansi(0))
|
||||
|
||||
|
||||
def make_color_fn(code):
|
||||
return lambda s: ansi_color(code, s)
|
||||
|
||||
|
||||
for (name, code) in get_pairs():
|
||||
globals()[name] = make_color_fn(code)
|
||||
|
||||
|
||||
def rainbow():
|
||||
cs = ['cyan', 'yellow', 'green', 'magenta', 'red', 'blue',
|
||||
'intense_cyan', 'intense_yellow', 'intense_green',
|
||||
'intense_magenta', 'intense_red', 'intense_blue']
|
||||
|
||||
for c in cs:
|
||||
yield globals()[c]
|
||||
38
fig/cli/command.py
Normal file
38
fig/cli/command.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
from docker import Client
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
|
||||
from ..project import Project
|
||||
from .docopt_command import DocoptCommand
|
||||
from .formatter import Formatter
|
||||
from .utils import cached_property, mkdir
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class Command(DocoptCommand):
|
||||
@cached_property
|
||||
def client(self):
|
||||
if os.environ.get('DOCKER_URL'):
|
||||
return Client(os.environ['DOCKER_URL'])
|
||||
else:
|
||||
return Client()
|
||||
|
||||
@cached_property
|
||||
def project(self):
|
||||
config = yaml.load(open('fig.yml'))
|
||||
return Project.from_config(self.project_name, config, self.client)
|
||||
|
||||
@cached_property
|
||||
def project_name(self):
|
||||
project = os.path.basename(os.getcwd())
|
||||
project = re.sub(r'[^a-zA-Z0-9]', '', project)
|
||||
if not project:
|
||||
project = 'default'
|
||||
return project
|
||||
|
||||
@cached_property
|
||||
def formatter(self):
|
||||
return Formatter()
|
||||
|
||||
52
fig/cli/docopt_command.py
Normal file
52
fig/cli/docopt_command.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import sys
|
||||
|
||||
from inspect import getdoc
|
||||
from docopt import docopt, DocoptExit
|
||||
|
||||
|
||||
def docopt_full_help(docstring, *args, **kwargs):
|
||||
try:
|
||||
return docopt(docstring, *args, **kwargs)
|
||||
except DocoptExit:
|
||||
raise SystemExit(docstring)
|
||||
|
||||
|
||||
class DocoptCommand(object):
|
||||
def docopt_options(self):
|
||||
return {'options_first': True}
|
||||
|
||||
def sys_dispatch(self):
|
||||
self.dispatch(sys.argv[1:], None)
|
||||
|
||||
def dispatch(self, argv, global_options):
|
||||
self.perform_command(*self.parse(argv, global_options))
|
||||
|
||||
def perform_command(self, options, command, handler, command_options):
|
||||
handler(command_options)
|
||||
|
||||
def parse(self, argv, global_options):
|
||||
options = docopt_full_help(getdoc(self), argv, **self.docopt_options())
|
||||
command = options['COMMAND']
|
||||
|
||||
if command is None:
|
||||
raise SystemExit(getdoc(self))
|
||||
|
||||
if not hasattr(self, command):
|
||||
raise NoSuchCommand(command, self)
|
||||
|
||||
handler = getattr(self, command)
|
||||
docstring = getdoc(handler)
|
||||
|
||||
if docstring is None:
|
||||
raise NoSuchCommand(command, self)
|
||||
|
||||
command_options = docopt_full_help(docstring, options['ARGS'], options_first=True)
|
||||
return (options, command, handler, command_options)
|
||||
|
||||
|
||||
class NoSuchCommand(Exception):
|
||||
def __init__(self, command, supercommand):
|
||||
super(NoSuchCommand, self).__init__("No such command: %s" % command)
|
||||
|
||||
self.command = command
|
||||
self.supercommand = supercommand
|
||||
6
fig/cli/errors.py
Normal file
6
fig/cli/errors.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from textwrap import dedent
|
||||
|
||||
|
||||
class UserError(Exception):
|
||||
def __init__(self, msg):
|
||||
self.msg = dedent(msg).strip()
|
||||
15
fig/cli/formatter.py
Normal file
15
fig/cli/formatter.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import texttable
|
||||
import os
|
||||
|
||||
|
||||
class Formatter(object):
|
||||
def table(self, headers, rows):
|
||||
height, width = os.popen('stty size', 'r').read().split()
|
||||
|
||||
table = texttable.Texttable(max_width=width)
|
||||
table.set_cols_dtype(['t' for h in headers])
|
||||
table.add_rows([headers] + rows)
|
||||
table.set_deco(table.HEADER)
|
||||
table.set_chars(['-', '|', '+', '-'])
|
||||
|
||||
return table.draw()
|
||||
66
fig/cli/log_printer.py
Normal file
66
fig/cli/log_printer.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import sys
|
||||
|
||||
from itertools import cycle
|
||||
|
||||
from .multiplexer import Multiplexer
|
||||
from . import colors
|
||||
|
||||
|
||||
class LogPrinter(object):
|
||||
def __init__(self, containers, attach_params=None):
|
||||
self.containers = containers
|
||||
self.attach_params = attach_params or {}
|
||||
self.generators = self._make_log_generators()
|
||||
|
||||
def run(self):
|
||||
mux = Multiplexer(self.generators)
|
||||
for line in mux.loop():
|
||||
sys.stdout.write(line)
|
||||
|
||||
def _make_log_generators(self):
|
||||
color_fns = cycle(colors.rainbow())
|
||||
generators = []
|
||||
|
||||
for container in self.containers:
|
||||
color_fn = color_fns.next()
|
||||
generators.append(self._make_log_generator(container, color_fn))
|
||||
|
||||
return generators
|
||||
|
||||
def _make_log_generator(self, container, color_fn):
|
||||
prefix = color_fn(container.name + " | ")
|
||||
websocket = self._attach(container)
|
||||
return (prefix + line for line in split_buffer(read_websocket(websocket), '\n'))
|
||||
|
||||
def _attach(self, container):
|
||||
params = {
|
||||
'stdin': False,
|
||||
'stdout': True,
|
||||
'stderr': True,
|
||||
'logs': False,
|
||||
'stream': True,
|
||||
}
|
||||
params.update(self.attach_params)
|
||||
params = dict((name, 1 if value else 0) for (name, value) in params.items())
|
||||
return container.attach_socket(params=params, ws=True)
|
||||
|
||||
def read_websocket(websocket):
|
||||
while True:
|
||||
data = websocket.recv()
|
||||
if data:
|
||||
yield data
|
||||
else:
|
||||
break
|
||||
|
||||
def split_buffer(reader, separator):
|
||||
buffered = ''
|
||||
|
||||
for data in reader:
|
||||
lines = (buffered + data).split(separator)
|
||||
for line in lines[:-1]:
|
||||
yield line + separator
|
||||
if len(lines) > 1:
|
||||
buffered = lines[-1]
|
||||
|
||||
if len(buffered) > 0:
|
||||
yield buffered
|
||||
253
fig/cli/main.py
Normal file
253
fig/cli/main.py
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import logging
|
||||
import sys
|
||||
import re
|
||||
|
||||
from inspect import getdoc
|
||||
|
||||
from .. import __version__
|
||||
from ..project import NoSuchService
|
||||
from .command import Command
|
||||
from .formatter import Formatter
|
||||
from .log_printer import LogPrinter
|
||||
|
||||
from docker.client import APIError
|
||||
from .errors import UserError
|
||||
from .docopt_command import NoSuchCommand
|
||||
from .socketclient import SocketClient
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(logging.Formatter())
|
||||
console_handler.setLevel(logging.INFO)
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.addHandler(console_handler)
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Disable requests logging
|
||||
logging.getLogger("requests").propagate = False
|
||||
|
||||
try:
|
||||
command = TopLevelCommand()
|
||||
command.sys_dispatch()
|
||||
except KeyboardInterrupt:
|
||||
log.error("\nAborting.")
|
||||
exit(1)
|
||||
except UserError, e:
|
||||
log.error(e.msg)
|
||||
exit(1)
|
||||
except NoSuchService, e:
|
||||
log.error(e.msg)
|
||||
exit(1)
|
||||
except NoSuchCommand, e:
|
||||
log.error("No such command: %s", e.command)
|
||||
log.error("")
|
||||
log.error("\n".join(parse_doc_section("commands:", getdoc(e.supercommand))))
|
||||
exit(1)
|
||||
except APIError, e:
|
||||
log.error(e.explanation)
|
||||
exit(1)
|
||||
|
||||
|
||||
# stolen from docopt master
|
||||
def parse_doc_section(name, source):
|
||||
pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)',
|
||||
re.IGNORECASE | re.MULTILINE)
|
||||
return [s.strip() for s in pattern.findall(source)]
|
||||
|
||||
|
||||
class TopLevelCommand(Command):
|
||||
""".
|
||||
|
||||
Usage:
|
||||
fig [options] [COMMAND] [ARGS...]
|
||||
fig -h|--help
|
||||
|
||||
Options:
|
||||
--verbose Show more output
|
||||
--version Print version and exit
|
||||
|
||||
Commands:
|
||||
up Create and start containers
|
||||
logs View output from containers
|
||||
ps List containers
|
||||
run Run a one-off command
|
||||
start Start services
|
||||
stop Stop services
|
||||
kill Kill containers
|
||||
rm Remove stopped containers
|
||||
|
||||
"""
|
||||
def docopt_options(self):
|
||||
options = super(TopLevelCommand, self).docopt_options()
|
||||
options['version'] = "fig %s" % __version__
|
||||
return options
|
||||
|
||||
def ps(self, options):
|
||||
"""
|
||||
List containers.
|
||||
|
||||
Usage: ps [options] [SERVICE...]
|
||||
|
||||
Options:
|
||||
-q Only display IDs
|
||||
"""
|
||||
containers = self.project.containers(service_names=options['SERVICE'], stopped=True) + self.project.containers(service_names=options['SERVICE'], one_off=True)
|
||||
|
||||
if options['-q']:
|
||||
for container in containers:
|
||||
print container.id
|
||||
else:
|
||||
headers = [
|
||||
'Name',
|
||||
'Command',
|
||||
'State',
|
||||
'Ports',
|
||||
]
|
||||
rows = []
|
||||
for container in containers:
|
||||
rows.append([
|
||||
container.name,
|
||||
container.human_readable_command,
|
||||
container.human_readable_state,
|
||||
container.human_readable_ports,
|
||||
])
|
||||
print Formatter().table(headers, rows)
|
||||
|
||||
def run(self, options):
|
||||
"""
|
||||
Run a one-off command.
|
||||
|
||||
Usage: run [options] SERVICE COMMAND [ARGS...]
|
||||
|
||||
Options:
|
||||
-d Detached mode: Run container in the background, print new container name
|
||||
"""
|
||||
service = self.project.get_service(options['SERVICE'])
|
||||
container_options = {
|
||||
'command': [options['COMMAND']] + options['ARGS'],
|
||||
'tty': not options['-d'],
|
||||
'stdin_open': not options['-d'],
|
||||
}
|
||||
container = service.create_container(one_off=True, **container_options)
|
||||
if options['-d']:
|
||||
service.start_container(container, ports=None)
|
||||
print container.name
|
||||
else:
|
||||
with self._attach_to_container(
|
||||
container.id,
|
||||
interactive=True,
|
||||
logs=True,
|
||||
raw=True
|
||||
) as c:
|
||||
service.start_container(container, ports=None)
|
||||
c.run()
|
||||
|
||||
def up(self, options):
|
||||
"""
|
||||
Create and start containers.
|
||||
|
||||
Usage: up [options] [SERVICE...]
|
||||
|
||||
Options:
|
||||
-d Detached mode: Run containers in the background, print new container names
|
||||
"""
|
||||
detached = options['-d']
|
||||
|
||||
unstarted = self.project.create_containers(service_names=options['SERVICE'])
|
||||
|
||||
if not detached:
|
||||
to_attach = self.project.containers(service_names=options['SERVICE']) + [c for (s, c) in unstarted]
|
||||
print "Attaching to", list_containers(to_attach)
|
||||
log_printer = LogPrinter(to_attach, attach_params={'logs': True})
|
||||
|
||||
for (s, c) in unstarted:
|
||||
s.start_container(c)
|
||||
|
||||
if detached:
|
||||
for (s, c) in unstarted:
|
||||
print c.name
|
||||
else:
|
||||
try:
|
||||
log_printer.run()
|
||||
finally:
|
||||
self.project.kill_and_remove(unstarted)
|
||||
|
||||
def start(self, options):
|
||||
"""
|
||||
Start existing containers.
|
||||
|
||||
Usage: start [SERVICE...]
|
||||
"""
|
||||
self.project.start(service_names=options['SERVICE'])
|
||||
|
||||
def stop(self, options):
|
||||
"""
|
||||
Stop running containers.
|
||||
|
||||
Usage: stop [SERVICE...]
|
||||
"""
|
||||
self.project.stop(service_names=options['SERVICE'])
|
||||
|
||||
def kill(self, options):
|
||||
"""
|
||||
Kill containers.
|
||||
|
||||
Usage: kill [SERVICE...]
|
||||
"""
|
||||
self.project.kill(service_names=options['SERVICE'])
|
||||
|
||||
def rm(self, options):
|
||||
"""
|
||||
Remove stopped containers
|
||||
|
||||
Usage: rm [SERVICE...]
|
||||
"""
|
||||
self.project.remove_stopped(service_names=options['SERVICE'])
|
||||
|
||||
def logs(self, options):
|
||||
"""
|
||||
View output from containers.
|
||||
|
||||
Usage: logs [SERVICE...]
|
||||
"""
|
||||
containers = self.project.containers(service_names=options['SERVICE'], stopped=False)
|
||||
print "Attaching to", list_containers(containers)
|
||||
LogPrinter(containers, attach_params={'logs': True}).run()
|
||||
|
||||
def _attach_to_container(self, container_id, interactive, logs=False, stream=True, raw=False):
|
||||
stdio = self.client.attach_socket(
|
||||
container_id,
|
||||
params={
|
||||
'stdin': 1 if interactive else 0,
|
||||
'stdout': 1,
|
||||
'stderr': 0,
|
||||
'logs': 1 if logs else 0,
|
||||
'stream': 1 if stream else 0
|
||||
},
|
||||
ws=True,
|
||||
)
|
||||
|
||||
stderr = self.client.attach_socket(
|
||||
container_id,
|
||||
params={
|
||||
'stdin': 0,
|
||||
'stdout': 0,
|
||||
'stderr': 1,
|
||||
'logs': 1 if logs else 0,
|
||||
'stream': 1 if stream else 0
|
||||
},
|
||||
ws=True,
|
||||
)
|
||||
|
||||
return SocketClient(
|
||||
socket_in=stdio,
|
||||
socket_out=stdio,
|
||||
socket_err=stderr,
|
||||
raw=raw,
|
||||
)
|
||||
|
||||
def list_containers(containers):
|
||||
return ", ".join(c.name for c in containers)
|
||||
32
fig/cli/multiplexer.py
Normal file
32
fig/cli/multiplexer.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from threading import Thread
|
||||
|
||||
try:
|
||||
from Queue import Queue, Empty
|
||||
except ImportError:
|
||||
from queue import Queue, Empty # Python 3.x
|
||||
|
||||
|
||||
class Multiplexer(object):
|
||||
def __init__(self, generators):
|
||||
self.generators = generators
|
||||
self.queue = Queue()
|
||||
|
||||
def loop(self):
|
||||
self._init_readers()
|
||||
|
||||
while True:
|
||||
try:
|
||||
yield self.queue.get(timeout=0.1)
|
||||
except Empty:
|
||||
pass
|
||||
|
||||
def _init_readers(self):
|
||||
for generator in self.generators:
|
||||
t = Thread(target=_enqueue_output, args=(generator, self.queue))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
|
||||
def _enqueue_output(generator, queue):
|
||||
for item in generator:
|
||||
queue.put(item)
|
||||
129
fig/cli/socketclient.py
Normal file
129
fig/cli/socketclient.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# Adapted from https://github.com/benthor/remotty/blob/master/socketclient.py
|
||||
|
||||
from select import select
|
||||
import sys
|
||||
import tty
|
||||
import fcntl
|
||||
import os
|
||||
import termios
|
||||
import threading
|
||||
import errno
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SocketClient:
|
||||
def __init__(self,
|
||||
socket_in=None,
|
||||
socket_out=None,
|
||||
socket_err=None,
|
||||
raw=True,
|
||||
):
|
||||
self.socket_in = socket_in
|
||||
self.socket_out = socket_out
|
||||
self.socket_err = socket_err
|
||||
self.raw = raw
|
||||
|
||||
self.stdin_fileno = sys.stdin.fileno()
|
||||
|
||||
def __enter__(self):
|
||||
self.create()
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, trace):
|
||||
self.destroy()
|
||||
|
||||
def create(self):
|
||||
if os.isatty(sys.stdin.fileno()):
|
||||
self.settings = termios.tcgetattr(sys.stdin.fileno())
|
||||
else:
|
||||
self.settings = None
|
||||
|
||||
if self.socket_in is not None:
|
||||
self.set_blocking(sys.stdin, False)
|
||||
self.set_blocking(sys.stdout, True)
|
||||
self.set_blocking(sys.stderr, True)
|
||||
|
||||
if self.raw:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
|
||||
def set_blocking(self, file, blocking):
|
||||
fd = file.fileno()
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
flags = (flags & ~os.O_NONBLOCK) if blocking else (flags | os.O_NONBLOCK)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, flags)
|
||||
|
||||
def run(self):
|
||||
if self.socket_in is not None:
|
||||
self.start_background_thread(target=self.send_ws, args=(self.socket_in, sys.stdin))
|
||||
|
||||
recv_threads = []
|
||||
|
||||
if self.socket_out is not None:
|
||||
recv_threads.append(self.start_background_thread(target=self.recv_ws, args=(self.socket_out, sys.stdout)))
|
||||
|
||||
if self.socket_err is not None:
|
||||
recv_threads.append(self.start_background_thread(target=self.recv_ws, args=(self.socket_err, sys.stderr)))
|
||||
|
||||
for t in recv_threads:
|
||||
t.join()
|
||||
|
||||
def start_background_thread(self, **kwargs):
|
||||
thread = threading.Thread(**kwargs)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
def recv_ws(self, socket, stream):
|
||||
try:
|
||||
while True:
|
||||
chunk = socket.recv()
|
||||
|
||||
if chunk:
|
||||
stream.write(chunk)
|
||||
stream.flush()
|
||||
else:
|
||||
break
|
||||
except Exception, e:
|
||||
log.debug(e)
|
||||
|
||||
def send_ws(self, socket, stream):
|
||||
while True:
|
||||
r, w, e = select([stream.fileno()], [], [])
|
||||
|
||||
if r:
|
||||
chunk = stream.read(1)
|
||||
|
||||
if chunk == '':
|
||||
socket.send_close()
|
||||
break
|
||||
else:
|
||||
try:
|
||||
socket.send(chunk)
|
||||
except Exception, e:
|
||||
if hasattr(e, 'errno') and e.errno == errno.EPIPE:
|
||||
break
|
||||
else:
|
||||
raise e
|
||||
|
||||
def destroy(self):
|
||||
if self.settings is not None:
|
||||
termios.tcsetattr(self.stdin_fileno, termios.TCSADRAIN, self.settings)
|
||||
|
||||
sys.stdout.flush()
|
||||
|
||||
if __name__ == '__main__':
|
||||
import websocket
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
sys.stderr.write("Usage: python socketclient.py WEBSOCKET_URL\n")
|
||||
exit(1)
|
||||
|
||||
url = sys.argv[1]
|
||||
socket = websocket.create_connection(url)
|
||||
|
||||
print "connected\r"
|
||||
|
||||
with SocketClient(socket, interactive=True) as client:
|
||||
client.run()
|
||||
76
fig/cli/utils.py
Normal file
76
fig/cli/utils.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import datetime
|
||||
import os
|
||||
|
||||
|
||||
def cached_property(f):
|
||||
"""
|
||||
returns a cached property that is calculated by function f
|
||||
http://code.activestate.com/recipes/576563-cached-property/
|
||||
"""
|
||||
def get(self):
|
||||
try:
|
||||
return self._property_cache[f]
|
||||
except AttributeError:
|
||||
self._property_cache = {}
|
||||
x = self._property_cache[f] = f(self)
|
||||
return x
|
||||
except KeyError:
|
||||
x = self._property_cache[f] = f(self)
|
||||
return x
|
||||
|
||||
return property(get)
|
||||
|
||||
|
||||
def yesno(prompt, default=None):
|
||||
"""
|
||||
Prompt the user for a yes or no.
|
||||
|
||||
Can optionally specify a default value, which will only be
|
||||
used if they enter a blank line.
|
||||
|
||||
Unrecognised input (anything other than "y", "n", "yes",
|
||||
"no" or "") will return None.
|
||||
"""
|
||||
answer = raw_input(prompt).strip().lower()
|
||||
|
||||
if answer == "y" or answer == "yes":
|
||||
return True
|
||||
elif answer == "n" or answer == "no":
|
||||
return False
|
||||
elif answer == "":
|
||||
return default
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
# http://stackoverflow.com/a/5164027
|
||||
def prettydate(d):
|
||||
diff = datetime.datetime.utcnow() - d
|
||||
s = diff.seconds
|
||||
if diff.days > 7 or diff.days < 0:
|
||||
return d.strftime('%d %b %y')
|
||||
elif diff.days == 1:
|
||||
return '1 day ago'
|
||||
elif diff.days > 1:
|
||||
return '{0} days ago'.format(diff.days)
|
||||
elif s <= 1:
|
||||
return 'just now'
|
||||
elif s < 60:
|
||||
return '{0} seconds ago'.format(s)
|
||||
elif s < 120:
|
||||
return '1 minute ago'
|
||||
elif s < 3600:
|
||||
return '{0} minutes ago'.format(s/60)
|
||||
elif s < 7200:
|
||||
return '1 hour ago'
|
||||
else:
|
||||
return '{0} hours ago'.format(s/3600)
|
||||
|
||||
|
||||
def mkdir(path, permissions=0700):
|
||||
if not os.path.exists(path):
|
||||
os.mkdir(path)
|
||||
|
||||
os.chmod(path, permissions)
|
||||
|
||||
return path
|
||||
Loading…
Add table
Add a link
Reference in a new issue