mirror of
https://github.com/rembo10/headphones.git
synced 2026-07-10 11:04:02 +01:00
Big Update: Automatic updating, Show what albums/songs you already have, Config fixes, Fixed restart & shutdown buttons
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
version_info = (2, 0, 0, 'rc', 2)
|
||||
version = '.'.join(str(n) for n in version_info[:3])
|
||||
release = version + ''.join(str(n) for n in version_info[3:])
|
||||
@@ -0,0 +1,64 @@
|
||||
__all__ = ('EVENT_SCHEDULER_START', 'EVENT_SCHEDULER_SHUTDOWN',
|
||||
'EVENT_JOBSTORE_ADDED', 'EVENT_JOBSTORE_REMOVED',
|
||||
'EVENT_JOBSTORE_JOB_ADDED', 'EVENT_JOBSTORE_JOB_REMOVED',
|
||||
'EVENT_JOB_EXECUTED', 'EVENT_JOB_ERROR', 'EVENT_JOB_MISSED',
|
||||
'EVENT_ALL', 'SchedulerEvent', 'JobStoreEvent', 'JobEvent')
|
||||
|
||||
|
||||
EVENT_SCHEDULER_START = 1 # The scheduler was started
|
||||
EVENT_SCHEDULER_SHUTDOWN = 2 # The scheduler was shut down
|
||||
EVENT_JOBSTORE_ADDED = 4 # A job store was added to the scheduler
|
||||
EVENT_JOBSTORE_REMOVED = 8 # A job store was removed from the scheduler
|
||||
EVENT_JOBSTORE_JOB_ADDED = 16 # A job was added to a job store
|
||||
EVENT_JOBSTORE_JOB_REMOVED = 32 # A job was removed from a job store
|
||||
EVENT_JOB_EXECUTED = 64 # A job was executed successfully
|
||||
EVENT_JOB_ERROR = 128 # A job raised an exception during execution
|
||||
EVENT_JOB_MISSED = 256 # A job's execution was missed
|
||||
EVENT_ALL = (EVENT_SCHEDULER_START | EVENT_SCHEDULER_SHUTDOWN |
|
||||
EVENT_JOBSTORE_ADDED | EVENT_JOBSTORE_REMOVED |
|
||||
EVENT_JOBSTORE_JOB_ADDED | EVENT_JOBSTORE_JOB_REMOVED |
|
||||
EVENT_JOB_EXECUTED | EVENT_JOB_ERROR | EVENT_JOB_MISSED)
|
||||
|
||||
|
||||
class SchedulerEvent(object):
|
||||
"""
|
||||
An event that concerns the scheduler itself.
|
||||
|
||||
:var code: the type code of this event
|
||||
"""
|
||||
def __init__(self, code):
|
||||
self.code = code
|
||||
|
||||
|
||||
class JobStoreEvent(SchedulerEvent):
|
||||
"""
|
||||
An event that concerns job stores.
|
||||
|
||||
:var alias: the alias of the job store involved
|
||||
:var job: the new job if a job was added
|
||||
"""
|
||||
def __init__(self, code, alias, job=None):
|
||||
SchedulerEvent.__init__(self, code)
|
||||
self.alias = alias
|
||||
if job:
|
||||
self.job = job
|
||||
|
||||
|
||||
class JobEvent(SchedulerEvent):
|
||||
"""
|
||||
An event that concerns the execution of individual jobs.
|
||||
|
||||
:var job: the job instance in question
|
||||
:var scheduled_run_time: the time when the job was scheduled to be run
|
||||
:var retval: the return value of the successfully executed job
|
||||
:var exception: the exception raised by the job
|
||||
:var traceback: the traceback object associated with the exception
|
||||
"""
|
||||
def __init__(self, code, job, scheduled_run_time, retval=None,
|
||||
exception=None, traceback=None):
|
||||
SchedulerEvent.__init__(self, code)
|
||||
self.job = job
|
||||
self.scheduled_run_time = scheduled_run_time
|
||||
self.retval = retval
|
||||
self.exception = exception
|
||||
self.traceback = traceback
|
||||
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Jobs represent scheduled tasks.
|
||||
"""
|
||||
|
||||
from threading import Lock
|
||||
from datetime import timedelta
|
||||
|
||||
from lib.apscheduler.util import to_unicode, ref_to_obj, get_callable_name,\
|
||||
obj_to_ref
|
||||
|
||||
|
||||
class MaxInstancesReachedError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Job(object):
|
||||
"""
|
||||
Encapsulates the actual Job along with its metadata. Job instances
|
||||
are created by the scheduler when adding jobs, and it should not be
|
||||
directly instantiated.
|
||||
|
||||
:param trigger: trigger that determines the execution times
|
||||
:param func: callable to call when the trigger is triggered
|
||||
:param args: list of positional arguments to call func with
|
||||
:param kwargs: dict of keyword arguments to call func with
|
||||
:param name: name of the job (optional)
|
||||
:param misfire_grace_time: seconds after the designated run time that
|
||||
the job is still allowed to be run
|
||||
:param coalesce: run once instead of many times if the scheduler determines
|
||||
that the job should be run more than once in succession
|
||||
:param max_runs: maximum number of times this job is allowed to be
|
||||
triggered
|
||||
:param max_instances: maximum number of concurrently running
|
||||
instances allowed for this job
|
||||
"""
|
||||
id = None
|
||||
next_run_time = None
|
||||
|
||||
def __init__(self, trigger, func, args, kwargs, misfire_grace_time,
|
||||
coalesce, name=None, max_runs=None, max_instances=1):
|
||||
if not trigger:
|
||||
raise ValueError('The trigger must not be None')
|
||||
if not hasattr(func, '__call__'):
|
||||
raise TypeError('func must be callable')
|
||||
if not hasattr(args, '__getitem__'):
|
||||
raise TypeError('args must be a list-like object')
|
||||
if not hasattr(kwargs, '__getitem__'):
|
||||
raise TypeError('kwargs must be a dict-like object')
|
||||
if misfire_grace_time <= 0:
|
||||
raise ValueError('misfire_grace_time must be a positive value')
|
||||
if max_runs is not None and max_runs <= 0:
|
||||
raise ValueError('max_runs must be a positive value')
|
||||
if max_instances <= 0:
|
||||
raise ValueError('max_instances must be a positive value')
|
||||
|
||||
self._lock = Lock()
|
||||
|
||||
self.trigger = trigger
|
||||
self.func = func
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.name = to_unicode(name or get_callable_name(func))
|
||||
self.misfire_grace_time = misfire_grace_time
|
||||
self.coalesce = coalesce
|
||||
self.max_runs = max_runs
|
||||
self.max_instances = max_instances
|
||||
self.runs = 0
|
||||
self.instances = 0
|
||||
|
||||
def compute_next_run_time(self, now):
|
||||
if self.runs == self.max_runs:
|
||||
self.next_run_time = None
|
||||
else:
|
||||
self.next_run_time = self.trigger.get_next_fire_time(now)
|
||||
|
||||
return self.next_run_time
|
||||
|
||||
def get_run_times(self, now):
|
||||
"""
|
||||
Computes the scheduled run times between ``next_run_time`` and ``now``.
|
||||
"""
|
||||
run_times = []
|
||||
run_time = self.next_run_time
|
||||
increment = timedelta(microseconds=1)
|
||||
while ((not self.max_runs or self.runs < self.max_runs) and
|
||||
run_time and run_time <= now):
|
||||
run_times.append(run_time)
|
||||
run_time = self.trigger.get_next_fire_time(run_time + increment)
|
||||
|
||||
return run_times
|
||||
|
||||
def add_instance(self):
|
||||
self._lock.acquire()
|
||||
try:
|
||||
if self.instances == self.max_instances:
|
||||
raise MaxInstancesReachedError
|
||||
self.instances += 1
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
def remove_instance(self):
|
||||
self._lock.acquire()
|
||||
try:
|
||||
assert self.instances > 0, 'Already at 0 instances'
|
||||
self.instances -= 1
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
def __getstate__(self):
|
||||
# Prevents the unwanted pickling of transient or unpicklable variables
|
||||
state = self.__dict__.copy()
|
||||
state.pop('instances', None)
|
||||
state.pop('func', None)
|
||||
state.pop('_lock', None)
|
||||
state['func_ref'] = obj_to_ref(self.func)
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
state['instances'] = 0
|
||||
state['func'] = ref_to_obj(state.pop('func_ref'))
|
||||
state['_lock'] = Lock()
|
||||
self.__dict__ = state
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Job):
|
||||
return self.id is not None and other.id == self.id or self is other
|
||||
return NotImplemented
|
||||
|
||||
def __repr__(self):
|
||||
return '<Job (name=%s, trigger=%s)>' % (self.name, repr(self.trigger))
|
||||
|
||||
def __str__(self):
|
||||
return '%s (trigger: %s, next run at: %s)' % (self.name,
|
||||
str(self.trigger), str(self.next_run_time))
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Abstract base class that provides the interface needed by all job stores.
|
||||
Job store methods are also documented here.
|
||||
"""
|
||||
|
||||
|
||||
class JobStore(object):
|
||||
def add_job(self, job):
|
||||
"""Adds the given job from this store."""
|
||||
raise NotImplementedError
|
||||
|
||||
def update_job(self, job):
|
||||
"""Persists the running state of the given job."""
|
||||
raise NotImplementedError
|
||||
|
||||
def remove_job(self, job):
|
||||
"""Removes the given jobs from this store."""
|
||||
raise NotImplementedError
|
||||
|
||||
def load_jobs(self):
|
||||
"""Loads jobs from this store into memory."""
|
||||
raise NotImplementedError
|
||||
|
||||
def close(self):
|
||||
"""Frees any resources still bound to this job store."""
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Stores jobs in a MongoDB database.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from lib.apscheduler.jobstores.base import JobStore
|
||||
from lib.apscheduler.job import Job
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError: # pragma: nocover
|
||||
import pickle
|
||||
|
||||
try:
|
||||
from bson.binary import Binary
|
||||
from pymongo.connection import Connection
|
||||
except ImportError: # pragma: nocover
|
||||
raise ImportError('MongoDBJobStore requires PyMongo installed')
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MongoDBJobStore(JobStore):
|
||||
def __init__(self, database='apscheduler', collection='jobs',
|
||||
connection=None, pickle_protocol=pickle.HIGHEST_PROTOCOL,
|
||||
**connect_args):
|
||||
self.jobs = []
|
||||
self.pickle_protocol = pickle_protocol
|
||||
|
||||
if not database:
|
||||
raise ValueError('The "database" parameter must not be empty')
|
||||
if not collection:
|
||||
raise ValueError('The "collection" parameter must not be empty')
|
||||
|
||||
if connection:
|
||||
self.connection = connection
|
||||
else:
|
||||
self.connection = Connection(**connect_args)
|
||||
|
||||
self.collection = self.connection[database][collection]
|
||||
|
||||
def add_job(self, job):
|
||||
job_dict = job.__getstate__()
|
||||
job_dict['trigger'] = Binary(pickle.dumps(job.trigger,
|
||||
self.pickle_protocol))
|
||||
job_dict['args'] = Binary(pickle.dumps(job.args,
|
||||
self.pickle_protocol))
|
||||
job_dict['kwargs'] = Binary(pickle.dumps(job.kwargs,
|
||||
self.pickle_protocol))
|
||||
job.id = self.collection.insert(job_dict)
|
||||
self.jobs.append(job)
|
||||
|
||||
def remove_job(self, job):
|
||||
self.collection.remove(job.id)
|
||||
self.jobs.remove(job)
|
||||
|
||||
def load_jobs(self):
|
||||
jobs = []
|
||||
for job_dict in self.collection.find():
|
||||
try:
|
||||
job = Job.__new__(Job)
|
||||
job_dict['id'] = job_dict.pop('_id')
|
||||
job_dict['trigger'] = pickle.loads(job_dict['trigger'])
|
||||
job_dict['args'] = pickle.loads(job_dict['args'])
|
||||
job_dict['kwargs'] = pickle.loads(job_dict['kwargs'])
|
||||
job.__setstate__(job_dict)
|
||||
jobs.append(job)
|
||||
except Exception:
|
||||
job_name = job_dict.get('name', '(unknown)')
|
||||
logger.exception('Unable to restore job "%s"', job_name)
|
||||
self.jobs = jobs
|
||||
|
||||
def update_job(self, job):
|
||||
spec = {'_id': job.id}
|
||||
document = {'$set': {'next_run_time': job.next_run_time},
|
||||
'$inc': {'runs': 1}}
|
||||
self.collection.update(spec, document)
|
||||
|
||||
def close(self):
|
||||
self.connection.disconnect()
|
||||
|
||||
def __repr__(self):
|
||||
connection = self.collection.database.connection
|
||||
return '<%s (connection=%s)>' % (self.__class__.__name__, connection)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Stores jobs in an array in RAM. Provides no persistence support.
|
||||
"""
|
||||
|
||||
from lib.apscheduler.jobstores.base import JobStore
|
||||
|
||||
|
||||
class RAMJobStore(JobStore):
|
||||
def __init__(self):
|
||||
self.jobs = []
|
||||
|
||||
def add_job(self, job):
|
||||
self.jobs.append(job)
|
||||
|
||||
def update_job(self, job):
|
||||
pass
|
||||
|
||||
def remove_job(self, job):
|
||||
self.jobs.remove(job)
|
||||
|
||||
def load_jobs(self):
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s>' % (self.__class__.__name__)
|
||||
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Stores jobs in a file governed by the :mod:`shelve` module.
|
||||
"""
|
||||
|
||||
import shelve
|
||||
import pickle
|
||||
import random
|
||||
import logging
|
||||
|
||||
from lib.apscheduler.jobstores.base import JobStore
|
||||
from lib.apscheduler.job import Job
|
||||
from lib.apscheduler.util import itervalues
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ShelveJobStore(JobStore):
|
||||
MAX_ID = 1000000
|
||||
|
||||
def __init__(self, path, pickle_protocol=pickle.HIGHEST_PROTOCOL):
|
||||
self.jobs = []
|
||||
self.path = path
|
||||
self.pickle_protocol = pickle_protocol
|
||||
self.store = shelve.open(path, 'c', self.pickle_protocol)
|
||||
|
||||
def _generate_id(self):
|
||||
id = None
|
||||
while not id:
|
||||
id = str(random.randint(1, self.MAX_ID))
|
||||
if not id in self.store:
|
||||
return id
|
||||
|
||||
def add_job(self, job):
|
||||
job.id = self._generate_id()
|
||||
self.jobs.append(job)
|
||||
self.store[job.id] = job.__getstate__()
|
||||
|
||||
def update_job(self, job):
|
||||
job_dict = self.store[job.id]
|
||||
job_dict['next_run_time'] = job.next_run_time
|
||||
job_dict['runs'] = job.runs
|
||||
self.store[job.id] = job_dict
|
||||
|
||||
def remove_job(self, job):
|
||||
del self.store[job.id]
|
||||
self.jobs.remove(job)
|
||||
|
||||
def load_jobs(self):
|
||||
jobs = []
|
||||
for job_dict in itervalues(self.store):
|
||||
try:
|
||||
job = Job.__new__(Job)
|
||||
job.__setstate__(job_dict)
|
||||
jobs.append(job)
|
||||
except Exception:
|
||||
job_name = job_dict.get('name', '(unknown)')
|
||||
logger.exception('Unable to restore job "%s"', job_name)
|
||||
|
||||
self.jobs = jobs
|
||||
|
||||
def close(self):
|
||||
self.store.close()
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s (path=%s)>' % (self.__class__.__name__, self.path)
|
||||
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Stores jobs in a database table using SQLAlchemy.
|
||||
"""
|
||||
import pickle
|
||||
import logging
|
||||
|
||||
from lib.apscheduler.jobstores.base import JobStore
|
||||
from lib.apscheduler.job import Job
|
||||
|
||||
try:
|
||||
from sqlalchemy import *
|
||||
except ImportError: # pragma: nocover
|
||||
raise ImportError('SQLAlchemyJobStore requires SQLAlchemy installed')
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SQLAlchemyJobStore(JobStore):
|
||||
def __init__(self, url=None, engine=None, tablename='apscheduler_jobs',
|
||||
metadata=None, pickle_protocol=pickle.HIGHEST_PROTOCOL):
|
||||
self.jobs = []
|
||||
self.pickle_protocol = pickle_protocol
|
||||
|
||||
if engine:
|
||||
self.engine = engine
|
||||
elif url:
|
||||
self.engine = create_engine(url)
|
||||
else:
|
||||
raise ValueError('Need either "engine" or "url" defined')
|
||||
|
||||
self.jobs_t = Table(tablename, metadata or MetaData(),
|
||||
Column('id', Integer,
|
||||
Sequence(tablename + '_id_seq', optional=True),
|
||||
primary_key=True),
|
||||
Column('trigger', PickleType(pickle_protocol, mutable=False),
|
||||
nullable=False),
|
||||
Column('func_ref', String(1024), nullable=False),
|
||||
Column('args', PickleType(pickle_protocol, mutable=False),
|
||||
nullable=False),
|
||||
Column('kwargs', PickleType(pickle_protocol, mutable=False),
|
||||
nullable=False),
|
||||
Column('name', Unicode(1024), unique=True),
|
||||
Column('misfire_grace_time', Integer, nullable=False),
|
||||
Column('coalesce', Boolean, nullable=False),
|
||||
Column('max_runs', Integer),
|
||||
Column('max_instances', Integer),
|
||||
Column('next_run_time', DateTime, nullable=False),
|
||||
Column('runs', BigInteger))
|
||||
|
||||
self.jobs_t.create(self.engine, True)
|
||||
|
||||
def add_job(self, job):
|
||||
job_dict = job.__getstate__()
|
||||
result = self.engine.execute(self.jobs_t.insert().values(**job_dict))
|
||||
job.id = result.inserted_primary_key[0]
|
||||
self.jobs.append(job)
|
||||
|
||||
def remove_job(self, job):
|
||||
delete = self.jobs_t.delete().where(self.jobs_t.c.id == job.id)
|
||||
self.engine.execute(delete)
|
||||
self.jobs.remove(job)
|
||||
|
||||
def load_jobs(self):
|
||||
jobs = []
|
||||
for row in self.engine.execute(select([self.jobs_t])):
|
||||
try:
|
||||
job = Job.__new__(Job)
|
||||
job_dict = dict(row.items())
|
||||
job.__setstate__(job_dict)
|
||||
jobs.append(job)
|
||||
except Exception:
|
||||
job_name = job_dict.get('name', '(unknown)')
|
||||
logger.exception('Unable to restore job "%s"', job_name)
|
||||
self.jobs = jobs
|
||||
|
||||
def update_job(self, job):
|
||||
job_dict = job.__getstate__()
|
||||
update = self.jobs_t.update().where(self.jobs_t.c.id == job.id).\
|
||||
values(next_run_time=job_dict['next_run_time'],
|
||||
runs=job_dict['runs'])
|
||||
self.engine.execute(update)
|
||||
|
||||
def close(self):
|
||||
self.engine.dispose()
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s (url=%s)>' % (self.__class__.__name__, self.engine.url)
|
||||
@@ -0,0 +1,559 @@
|
||||
"""
|
||||
This module is the main part of the library. It houses the Scheduler class
|
||||
and related exceptions.
|
||||
"""
|
||||
|
||||
from threading import Thread, Event, Lock
|
||||
from datetime import datetime, timedelta
|
||||
from logging import getLogger
|
||||
import os
|
||||
import sys
|
||||
|
||||
from lib.apscheduler.util import *
|
||||
from lib.apscheduler.triggers import SimpleTrigger, IntervalTrigger, CronTrigger
|
||||
from lib.apscheduler.jobstores.ram_store import RAMJobStore
|
||||
from lib.apscheduler.job import Job, MaxInstancesReachedError
|
||||
from lib.apscheduler.events import *
|
||||
from lib.apscheduler.threadpool import ThreadPool
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class SchedulerAlreadyRunningError(Exception):
|
||||
"""
|
||||
Raised when attempting to start or configure the scheduler when it's
|
||||
already running.
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return 'Scheduler is already running'
|
||||
|
||||
|
||||
class Scheduler(object):
|
||||
"""
|
||||
This class is responsible for scheduling jobs and triggering
|
||||
their execution.
|
||||
"""
|
||||
|
||||
_stopped = False
|
||||
_thread = None
|
||||
|
||||
def __init__(self, gconfig={}, **options):
|
||||
self._wakeup = Event()
|
||||
self._jobstores = {}
|
||||
self._jobstores_lock = Lock()
|
||||
self._listeners = []
|
||||
self._listeners_lock = Lock()
|
||||
self._pending_jobs = []
|
||||
self.configure(gconfig, **options)
|
||||
|
||||
def configure(self, gconfig={}, **options):
|
||||
"""
|
||||
Reconfigures the scheduler with the given options. Can only be done
|
||||
when the scheduler isn't running.
|
||||
"""
|
||||
if self.running:
|
||||
raise SchedulerAlreadyRunningError
|
||||
|
||||
# Set general options
|
||||
config = combine_opts(gconfig, 'apscheduler.', options)
|
||||
self.misfire_grace_time = int(config.pop('misfire_grace_time', 1))
|
||||
self.coalesce = asbool(config.pop('coalesce', True))
|
||||
self.daemonic = asbool(config.pop('daemonic', True))
|
||||
|
||||
# Configure the thread pool
|
||||
if 'threadpool' in config:
|
||||
self._threadpool = maybe_ref(config['threadpool'])
|
||||
else:
|
||||
threadpool_opts = combine_opts(config, 'threadpool.')
|
||||
self._threadpool = ThreadPool(**threadpool_opts)
|
||||
|
||||
# Configure job stores
|
||||
jobstore_opts = combine_opts(config, 'jobstore.')
|
||||
jobstores = {}
|
||||
for key, value in jobstore_opts.items():
|
||||
store_name, option = key.split('.', 1)
|
||||
opts_dict = jobstores.setdefault(store_name, {})
|
||||
opts_dict[option] = value
|
||||
|
||||
for alias, opts in jobstores.items():
|
||||
classname = opts.pop('class')
|
||||
cls = maybe_ref(classname)
|
||||
jobstore = cls(**opts)
|
||||
self.add_jobstore(jobstore, alias, True)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Starts the scheduler in a new thread.
|
||||
"""
|
||||
if self.running:
|
||||
raise SchedulerAlreadyRunningError
|
||||
|
||||
# Create a RAMJobStore as the default if there is no default job store
|
||||
if not 'default' in self._jobstores:
|
||||
self.add_jobstore(RAMJobStore(), 'default', True)
|
||||
|
||||
# Schedule all pending jobs
|
||||
for job, jobstore in self._pending_jobs:
|
||||
self._real_add_job(job, jobstore, False)
|
||||
del self._pending_jobs[:]
|
||||
|
||||
self._stopped = False
|
||||
self._thread = Thread(target=self._main_loop, name='APScheduler')
|
||||
self._thread.setDaemon(self.daemonic)
|
||||
self._thread.start()
|
||||
|
||||
def shutdown(self, wait=True, shutdown_threadpool=True):
|
||||
"""
|
||||
Shuts down the scheduler and terminates the thread.
|
||||
Does not interrupt any currently running jobs.
|
||||
|
||||
:param wait: ``True`` to wait until all currently executing jobs have
|
||||
finished (if ``shutdown_threadpool`` is also ``True``)
|
||||
:param shutdown_threadpool: ``True`` to shut down the thread pool
|
||||
"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
self._stopped = True
|
||||
self._wakeup.set()
|
||||
|
||||
# Shut down the thread pool
|
||||
if shutdown_threadpool:
|
||||
self._threadpool.shutdown(wait)
|
||||
|
||||
# Wait until the scheduler thread terminates
|
||||
self._thread.join()
|
||||
|
||||
@property
|
||||
def running(self):
|
||||
return not self._stopped and self._thread and self._thread.isAlive()
|
||||
|
||||
def add_jobstore(self, jobstore, alias, quiet=False):
|
||||
"""
|
||||
Adds a job store to this scheduler.
|
||||
|
||||
:param jobstore: job store to be added
|
||||
:param alias: alias for the job store
|
||||
:param quiet: True to suppress scheduler thread wakeup
|
||||
:type jobstore: instance of
|
||||
:class:`~apscheduler.jobstores.base.JobStore`
|
||||
:type alias: str
|
||||
"""
|
||||
self._jobstores_lock.acquire()
|
||||
try:
|
||||
if alias in self._jobstores:
|
||||
raise KeyError('Alias "%s" is already in use' % alias)
|
||||
self._jobstores[alias] = jobstore
|
||||
jobstore.load_jobs()
|
||||
finally:
|
||||
self._jobstores_lock.release()
|
||||
|
||||
# Notify listeners that a new job store has been added
|
||||
self._notify_listeners(JobStoreEvent(EVENT_JOBSTORE_ADDED, alias))
|
||||
|
||||
# Notify the scheduler so it can scan the new job store for jobs
|
||||
if not quiet:
|
||||
self._wakeup.set()
|
||||
|
||||
def remove_jobstore(self, alias):
|
||||
"""
|
||||
Removes the job store by the given alias from this scheduler.
|
||||
|
||||
:type alias: str
|
||||
"""
|
||||
self._jobstores_lock.acquire()
|
||||
try:
|
||||
try:
|
||||
del self._jobstores[alias]
|
||||
except KeyError:
|
||||
raise KeyError('No such job store: %s' % alias)
|
||||
finally:
|
||||
self._jobstores_lock.release()
|
||||
|
||||
# Notify listeners that a job store has been removed
|
||||
self._notify_listeners(JobStoreEvent(EVENT_JOBSTORE_REMOVED, alias))
|
||||
|
||||
def add_listener(self, callback, mask=EVENT_ALL):
|
||||
"""
|
||||
Adds a listener for scheduler events. When a matching event occurs,
|
||||
``callback`` is executed with the event object as its sole argument.
|
||||
If the ``mask`` parameter is not provided, the callback will receive
|
||||
events of all types.
|
||||
|
||||
:param callback: any callable that takes one argument
|
||||
:param mask: bitmask that indicates which events should be listened to
|
||||
"""
|
||||
self._listeners_lock.acquire()
|
||||
try:
|
||||
self._listeners.append((callback, mask))
|
||||
finally:
|
||||
self._listeners_lock.release()
|
||||
|
||||
def remove_listener(self, callback):
|
||||
"""
|
||||
Removes a previously added event listener.
|
||||
"""
|
||||
self._listeners_lock.acquire()
|
||||
try:
|
||||
for i, (cb, _) in enumerate(self._listeners):
|
||||
if callback == cb:
|
||||
del self._listeners[i]
|
||||
finally:
|
||||
self._listeners_lock.release()
|
||||
|
||||
def _notify_listeners(self, event):
|
||||
self._listeners_lock.acquire()
|
||||
try:
|
||||
listeners = tuple(self._listeners)
|
||||
finally:
|
||||
self._listeners_lock.release()
|
||||
|
||||
for cb, mask in listeners:
|
||||
if event.code & mask:
|
||||
try:
|
||||
cb(event)
|
||||
except:
|
||||
logger.exception('Error notifying listener')
|
||||
|
||||
def _real_add_job(self, job, jobstore, wakeup):
|
||||
job.compute_next_run_time(datetime.now())
|
||||
if not job.next_run_time:
|
||||
raise ValueError('Not adding job since it would never be run')
|
||||
|
||||
self._jobstores_lock.acquire()
|
||||
try:
|
||||
try:
|
||||
store = self._jobstores[jobstore]
|
||||
except KeyError:
|
||||
raise KeyError('No such job store: %s' % jobstore)
|
||||
store.add_job(job)
|
||||
finally:
|
||||
self._jobstores_lock.release()
|
||||
|
||||
# Notify listeners that a new job has been added
|
||||
event = JobStoreEvent(EVENT_JOBSTORE_JOB_ADDED, jobstore, job)
|
||||
self._notify_listeners(event)
|
||||
|
||||
logger.info('Added job "%s" to job store "%s"', job, jobstore)
|
||||
|
||||
# Notify the scheduler about the new job
|
||||
if wakeup:
|
||||
self._wakeup.set()
|
||||
|
||||
def add_job(self, trigger, func, args, kwargs, jobstore='default',
|
||||
**options):
|
||||
"""
|
||||
Adds the given job to the job list and notifies the scheduler thread.
|
||||
|
||||
:param trigger: alias of the job store to store the job in
|
||||
:param func: callable to run at the given time
|
||||
:param args: list of positional arguments to call func with
|
||||
:param kwargs: dict of keyword arguments to call func with
|
||||
:param jobstore: alias of the job store to store the job in
|
||||
:rtype: :class:`~apscheduler.job.Job`
|
||||
"""
|
||||
job = Job(trigger, func, args or [], kwargs or {},
|
||||
options.pop('misfire_grace_time', self.misfire_grace_time),
|
||||
options.pop('coalesce', self.coalesce), **options)
|
||||
if not self.running:
|
||||
self._pending_jobs.append((job, jobstore))
|
||||
logger.info('Adding job tentatively -- it will be properly '
|
||||
'scheduled when the scheduler starts')
|
||||
else:
|
||||
self._real_add_job(job, jobstore, True)
|
||||
return job
|
||||
|
||||
def _remove_job(self, job, alias, jobstore):
|
||||
jobstore.remove_job(job)
|
||||
|
||||
# Notify listeners that a job has been removed
|
||||
event = JobStoreEvent(EVENT_JOBSTORE_JOB_REMOVED, alias, job)
|
||||
self._notify_listeners(event)
|
||||
|
||||
logger.info('Removed job "%s"', job)
|
||||
|
||||
def add_date_job(self, func, date, args=None, kwargs=None, **options):
|
||||
"""
|
||||
Schedules a job to be completed on a specific date and time.
|
||||
|
||||
:param func: callable to run at the given time
|
||||
:param date: the date/time to run the job at
|
||||
:param name: name of the job
|
||||
:param jobstore: stored the job in the named (or given) job store
|
||||
:param misfire_grace_time: seconds after the designated run time that
|
||||
the job is still allowed to be run
|
||||
:type date: :class:`datetime.date`
|
||||
:rtype: :class:`~apscheduler.job.Job`
|
||||
"""
|
||||
trigger = SimpleTrigger(date)
|
||||
return self.add_job(trigger, func, args, kwargs, **options)
|
||||
|
||||
def add_interval_job(self, func, weeks=0, days=0, hours=0, minutes=0,
|
||||
seconds=0, start_date=None, args=None, kwargs=None,
|
||||
**options):
|
||||
"""
|
||||
Schedules a job to be completed on specified intervals.
|
||||
|
||||
:param func: callable to run
|
||||
:param weeks: number of weeks to wait
|
||||
:param days: number of days to wait
|
||||
:param hours: number of hours to wait
|
||||
:param minutes: number of minutes to wait
|
||||
:param seconds: number of seconds to wait
|
||||
:param start_date: when to first execute the job and start the
|
||||
counter (default is after the given interval)
|
||||
:param args: list of positional arguments to call func with
|
||||
:param kwargs: dict of keyword arguments to call func with
|
||||
:param name: name of the job
|
||||
:param jobstore: alias of the job store to add the job to
|
||||
:param misfire_grace_time: seconds after the designated run time that
|
||||
the job is still allowed to be run
|
||||
:rtype: :class:`~apscheduler.job.Job`
|
||||
"""
|
||||
interval = timedelta(weeks=weeks, days=days, hours=hours,
|
||||
minutes=minutes, seconds=seconds)
|
||||
trigger = IntervalTrigger(interval, start_date)
|
||||
return self.add_job(trigger, func, args, kwargs, **options)
|
||||
|
||||
def add_cron_job(self, func, year='*', month='*', day='*', week='*',
|
||||
day_of_week='*', hour='*', minute='*', second='*',
|
||||
start_date=None, args=None, kwargs=None, **options):
|
||||
"""
|
||||
Schedules a job to be completed on times that match the given
|
||||
expressions.
|
||||
|
||||
:param func: callable to run
|
||||
:param year: year to run on
|
||||
:param month: month to run on (0 = January)
|
||||
:param day: day of month to run on
|
||||
:param week: week of the year to run on
|
||||
:param day_of_week: weekday to run on (0 = Monday)
|
||||
:param hour: hour to run on
|
||||
:param second: second to run on
|
||||
:param args: list of positional arguments to call func with
|
||||
:param kwargs: dict of keyword arguments to call func with
|
||||
:param name: name of the job
|
||||
:param jobstore: alias of the job store to add the job to
|
||||
:param misfire_grace_time: seconds after the designated run time that
|
||||
the job is still allowed to be run
|
||||
:return: the scheduled job
|
||||
:rtype: :class:`~apscheduler.job.Job`
|
||||
"""
|
||||
trigger = CronTrigger(year=year, month=month, day=day, week=week,
|
||||
day_of_week=day_of_week, hour=hour,
|
||||
minute=minute, second=second,
|
||||
start_date=start_date)
|
||||
return self.add_job(trigger, func, args, kwargs, **options)
|
||||
|
||||
def cron_schedule(self, **options):
|
||||
"""
|
||||
Decorator version of :meth:`add_cron_job`.
|
||||
This decorator does not wrap its host function.
|
||||
Unscheduling decorated functions is possible by passing the ``job``
|
||||
attribute of the scheduled function to :meth:`unschedule_job`.
|
||||
"""
|
||||
def inner(func):
|
||||
func.job = self.add_cron_job(func, **options)
|
||||
return func
|
||||
return inner
|
||||
|
||||
def interval_schedule(self, **options):
|
||||
"""
|
||||
Decorator version of :meth:`add_interval_job`.
|
||||
This decorator does not wrap its host function.
|
||||
Unscheduling decorated functions is possible by passing the ``job``
|
||||
attribute of the scheduled function to :meth:`unschedule_job`.
|
||||
"""
|
||||
def inner(func):
|
||||
func.job = self.add_interval_job(func, **options)
|
||||
return func
|
||||
return inner
|
||||
|
||||
def get_jobs(self):
|
||||
"""
|
||||
Returns a list of all scheduled jobs.
|
||||
|
||||
:return: list of :class:`~apscheduler.job.Job` objects
|
||||
"""
|
||||
self._jobstores_lock.acquire()
|
||||
try:
|
||||
jobs = []
|
||||
for jobstore in itervalues(self._jobstores):
|
||||
jobs.extend(jobstore.jobs)
|
||||
return jobs
|
||||
finally:
|
||||
self._jobstores_lock.release()
|
||||
|
||||
def unschedule_job(self, job):
|
||||
"""
|
||||
Removes a job, preventing it from being run any more.
|
||||
"""
|
||||
self._jobstores_lock.acquire()
|
||||
try:
|
||||
for alias, jobstore in iteritems(self._jobstores):
|
||||
if job in list(jobstore.jobs):
|
||||
self._remove_job(job, alias, jobstore)
|
||||
return
|
||||
finally:
|
||||
self._jobstores_lock.release()
|
||||
|
||||
raise KeyError('Job "%s" is not scheduled in any job store' % job)
|
||||
|
||||
def unschedule_func(self, func):
|
||||
"""
|
||||
Removes all jobs that would execute the given function.
|
||||
"""
|
||||
found = False
|
||||
self._jobstores_lock.acquire()
|
||||
try:
|
||||
for alias, jobstore in iteritems(self._jobstores):
|
||||
for job in list(jobstore.jobs):
|
||||
if job.func == func:
|
||||
self._remove_job(job, alias, jobstore)
|
||||
found = True
|
||||
finally:
|
||||
self._jobstores_lock.release()
|
||||
|
||||
if not found:
|
||||
raise KeyError('The given function is not scheduled in this '
|
||||
'scheduler')
|
||||
|
||||
def print_jobs(self, out=None):
|
||||
"""
|
||||
Prints out a textual listing of all jobs currently scheduled on this
|
||||
scheduler.
|
||||
|
||||
:param out: a file-like object to print to (defaults to **sys.stdout**
|
||||
if nothing is given)
|
||||
"""
|
||||
out = out or sys.stdout
|
||||
job_strs = []
|
||||
self._jobstores_lock.acquire()
|
||||
try:
|
||||
for alias, jobstore in iteritems(self._jobstores):
|
||||
job_strs.append('Jobstore %s:' % alias)
|
||||
if jobstore.jobs:
|
||||
for job in jobstore.jobs:
|
||||
job_strs.append(' %s' % job)
|
||||
else:
|
||||
job_strs.append(' No scheduled jobs')
|
||||
finally:
|
||||
self._jobstores_lock.release()
|
||||
|
||||
out.write(os.linesep.join(job_strs))
|
||||
|
||||
def _run_job(self, job, run_times):
|
||||
"""
|
||||
Acts as a harness that runs the actual job code in a thread.
|
||||
"""
|
||||
for run_time in run_times:
|
||||
# See if the job missed its run time window, and handle possible
|
||||
# misfires accordingly
|
||||
difference = datetime.now() - run_time
|
||||
grace_time = timedelta(seconds=job.misfire_grace_time)
|
||||
if difference > grace_time:
|
||||
# Notify listeners about a missed run
|
||||
event = JobEvent(EVENT_JOB_MISSED, job, run_time)
|
||||
self._notify_listeners(event)
|
||||
logger.warning('Run time of job "%s" was missed by %s',
|
||||
job, difference)
|
||||
else:
|
||||
try:
|
||||
job.add_instance()
|
||||
except MaxInstancesReachedError:
|
||||
event = JobEvent(EVENT_JOB_MISSED, job, run_time)
|
||||
self._notify_listeners(event)
|
||||
logger.warning('Execution of job "%s" skipped: '
|
||||
'maximum number of running instances '
|
||||
'reached (%d)', job, job.max_instances)
|
||||
break
|
||||
|
||||
logger.info('Running job "%s" (scheduled at %s)', job,
|
||||
run_time)
|
||||
|
||||
try:
|
||||
retval = job.func(*job.args, **job.kwargs)
|
||||
except:
|
||||
# Notify listeners about the exception
|
||||
exc, tb = sys.exc_info()[1:]
|
||||
event = JobEvent(EVENT_JOB_ERROR, job, run_time,
|
||||
exception=exc, traceback=tb)
|
||||
self._notify_listeners(event)
|
||||
|
||||
logger.exception('Job "%s" raised an exception', job)
|
||||
else:
|
||||
# Notify listeners about successful execution
|
||||
event = JobEvent(EVENT_JOB_EXECUTED, job, run_time,
|
||||
retval=retval)
|
||||
self._notify_listeners(event)
|
||||
|
||||
logger.info('Job "%s" executed successfully', job)
|
||||
|
||||
job.remove_instance()
|
||||
|
||||
# If coalescing is enabled, don't attempt any further runs
|
||||
if job.coalesce:
|
||||
break
|
||||
|
||||
def _process_jobs(self, now):
|
||||
"""
|
||||
Iterates through jobs in every jobstore, starts pending jobs
|
||||
and figures out the next wakeup time.
|
||||
"""
|
||||
next_wakeup_time = None
|
||||
self._jobstores_lock.acquire()
|
||||
try:
|
||||
for alias, jobstore in iteritems(self._jobstores):
|
||||
for job in tuple(jobstore.jobs):
|
||||
run_times = job.get_run_times(now)
|
||||
if run_times:
|
||||
self._threadpool.submit(self._run_job, job, run_times)
|
||||
|
||||
# Increase the job's run count
|
||||
if job.coalesce:
|
||||
job.runs += 1
|
||||
else:
|
||||
job.runs += len(run_times)
|
||||
|
||||
# Update the job, but don't keep finished jobs around
|
||||
if job.compute_next_run_time(now + timedelta(microseconds=1)):
|
||||
jobstore.update_job(job)
|
||||
else:
|
||||
self._remove_job(job, alias, jobstore)
|
||||
|
||||
if not next_wakeup_time:
|
||||
next_wakeup_time = job.next_run_time
|
||||
elif job.next_run_time:
|
||||
next_wakeup_time = min(next_wakeup_time,
|
||||
job.next_run_time)
|
||||
return next_wakeup_time
|
||||
finally:
|
||||
self._jobstores_lock.release()
|
||||
|
||||
def _main_loop(self):
|
||||
"""Executes jobs on schedule."""
|
||||
|
||||
logger.info('Scheduler started')
|
||||
self._notify_listeners(SchedulerEvent(EVENT_SCHEDULER_START))
|
||||
|
||||
self._wakeup.clear()
|
||||
while not self._stopped:
|
||||
logger.debug('Looking for jobs to run')
|
||||
now = datetime.now()
|
||||
next_wakeup_time = self._process_jobs(now)
|
||||
|
||||
# Sleep until the next job is scheduled to be run,
|
||||
# a new job is added or the scheduler is stopped
|
||||
if next_wakeup_time is not None:
|
||||
wait_seconds = time_difference(next_wakeup_time, now)
|
||||
logger.debug('Next wakeup is due at %s (in %f seconds)',
|
||||
next_wakeup_time, wait_seconds)
|
||||
self._wakeup.wait(wait_seconds)
|
||||
else:
|
||||
logger.debug('No jobs; waiting until a job is added')
|
||||
self._wakeup.wait()
|
||||
self._wakeup.clear()
|
||||
|
||||
logger.info('Scheduler has been shut down')
|
||||
self._notify_listeners(SchedulerEvent(EVENT_SCHEDULER_SHUTDOWN))
|
||||
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Generic thread pool class. Modeled after Java's ThreadPoolExecutor.
|
||||
Please note that this ThreadPool does *not* fully implement the PEP 3148
|
||||
ThreadPool!
|
||||
"""
|
||||
|
||||
from threading import Thread, Lock, currentThread
|
||||
from weakref import ref
|
||||
import logging
|
||||
import atexit
|
||||
|
||||
try:
|
||||
from queue import Queue, Empty
|
||||
except ImportError:
|
||||
from Queue import Queue, Empty
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_threadpools = set()
|
||||
|
||||
|
||||
# Worker threads are daemonic in order to let the interpreter exit without
|
||||
# an explicit shutdown of the thread pool. The following trick is necessary
|
||||
# to allow worker threads to finish cleanly.
|
||||
def _shutdown_all():
|
||||
for pool_ref in tuple(_threadpools):
|
||||
pool = pool_ref()
|
||||
if pool:
|
||||
pool.shutdown()
|
||||
|
||||
atexit.register(_shutdown_all)
|
||||
|
||||
|
||||
class ThreadPool(object):
|
||||
def __init__(self, core_threads=0, max_threads=20, keepalive=1):
|
||||
"""
|
||||
:param core_threads: maximum number of persistent threads in the pool
|
||||
:param max_threads: maximum number of total threads in the pool
|
||||
:param thread_class: callable that creates a Thread object
|
||||
:param keepalive: seconds to keep non-core worker threads waiting
|
||||
for new tasks
|
||||
"""
|
||||
self.core_threads = core_threads
|
||||
self.max_threads = max(max_threads, core_threads, 1)
|
||||
self.keepalive = keepalive
|
||||
self._queue = Queue()
|
||||
self._threads_lock = Lock()
|
||||
self._threads = set()
|
||||
self._shutdown = False
|
||||
|
||||
_threadpools.add(ref(self))
|
||||
logger.info('Started thread pool with %d core threads and %s maximum '
|
||||
'threads', core_threads, max_threads or 'unlimited')
|
||||
|
||||
def _adjust_threadcount(self):
|
||||
self._threads_lock.acquire()
|
||||
try:
|
||||
if self.num_threads < self.max_threads:
|
||||
self._add_thread(self.num_threads < self.core_threads)
|
||||
finally:
|
||||
self._threads_lock.release()
|
||||
|
||||
def _add_thread(self, core):
|
||||
t = Thread(target=self._run_jobs, args=(core,))
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
self._threads.add(t)
|
||||
|
||||
def _run_jobs(self, core):
|
||||
logger.debug('Started worker thread')
|
||||
block = True
|
||||
timeout = None
|
||||
if not core:
|
||||
block = self.keepalive > 0
|
||||
timeout = self.keepalive
|
||||
|
||||
while True:
|
||||
try:
|
||||
func, args, kwargs = self._queue.get(block, timeout)
|
||||
except Empty:
|
||||
break
|
||||
|
||||
if self._shutdown:
|
||||
break
|
||||
|
||||
try:
|
||||
func(*args, **kwargs)
|
||||
except:
|
||||
logger.exception('Error in worker thread')
|
||||
|
||||
self._threads_lock.acquire()
|
||||
self._threads.remove(currentThread())
|
||||
self._threads_lock.release()
|
||||
|
||||
logger.debug('Exiting worker thread')
|
||||
|
||||
@property
|
||||
def num_threads(self):
|
||||
return len(self._threads)
|
||||
|
||||
def submit(self, func, *args, **kwargs):
|
||||
if self._shutdown:
|
||||
raise RuntimeError('Cannot schedule new tasks after shutdown')
|
||||
|
||||
self._queue.put((func, args, kwargs))
|
||||
self._adjust_threadcount()
|
||||
|
||||
def shutdown(self, wait=True):
|
||||
if self._shutdown:
|
||||
return
|
||||
|
||||
logging.info('Shutting down thread pool')
|
||||
self._shutdown = True
|
||||
_threadpools.remove(ref(self))
|
||||
|
||||
self._threads_lock.acquire()
|
||||
for _ in range(self.num_threads):
|
||||
self._queue.put((None, None, None))
|
||||
self._threads_lock.release()
|
||||
|
||||
if wait:
|
||||
self._threads_lock.acquire()
|
||||
threads = tuple(self._threads)
|
||||
self._threads_lock.release()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
def __repr__(self):
|
||||
if self.max_threads:
|
||||
threadcount = '%d/%d' % (self.num_threads, self.max_threads)
|
||||
else:
|
||||
threadcount = '%d' % self.num_threads
|
||||
|
||||
return '<ThreadPool at %x; threads=%s>' % (id(self), threadcount)
|
||||
@@ -0,0 +1,3 @@
|
||||
from lib.apscheduler.triggers.cron import CronTrigger
|
||||
from lib.apscheduler.triggers.interval import IntervalTrigger
|
||||
from lib.apscheduler.triggers.simple import SimpleTrigger
|
||||
@@ -0,0 +1,135 @@
|
||||
from datetime import date, datetime
|
||||
|
||||
from lib.apscheduler.triggers.cron.fields import *
|
||||
from lib.apscheduler.util import datetime_ceil, convert_to_datetime
|
||||
|
||||
|
||||
class CronTrigger(object):
|
||||
FIELD_NAMES = ('year', 'month', 'day', 'week', 'day_of_week', 'hour',
|
||||
'minute', 'second')
|
||||
FIELDS_MAP = {'year': BaseField,
|
||||
'month': BaseField,
|
||||
'week': WeekField,
|
||||
'day': DayOfMonthField,
|
||||
'day_of_week': DayOfWeekField,
|
||||
'hour': BaseField,
|
||||
'minute': BaseField,
|
||||
'second': BaseField}
|
||||
|
||||
def __init__(self, **values):
|
||||
self.start_date = values.pop('start_date', None)
|
||||
if self.start_date:
|
||||
self.start_date = convert_to_datetime(self.start_date)
|
||||
|
||||
self.fields = []
|
||||
for field_name in self.FIELD_NAMES:
|
||||
if field_name in values:
|
||||
exprs = values.pop(field_name)
|
||||
is_default = False
|
||||
elif not values:
|
||||
exprs = DEFAULT_VALUES[field_name]
|
||||
is_default = True
|
||||
else:
|
||||
exprs = '*'
|
||||
is_default = True
|
||||
|
||||
field_class = self.FIELDS_MAP[field_name]
|
||||
field = field_class(field_name, exprs, is_default)
|
||||
self.fields.append(field)
|
||||
|
||||
def _increment_field_value(self, dateval, fieldnum):
|
||||
"""
|
||||
Increments the designated field and resets all less significant fields
|
||||
to their minimum values.
|
||||
|
||||
:type dateval: datetime
|
||||
:type fieldnum: int
|
||||
:type amount: int
|
||||
:rtype: tuple
|
||||
:return: a tuple containing the new date, and the number of the field
|
||||
that was actually incremented
|
||||
"""
|
||||
i = 0
|
||||
values = {}
|
||||
while i < len(self.fields):
|
||||
field = self.fields[i]
|
||||
if not field.REAL:
|
||||
if i == fieldnum:
|
||||
fieldnum -= 1
|
||||
i -= 1
|
||||
else:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if i < fieldnum:
|
||||
values[field.name] = field.get_value(dateval)
|
||||
i += 1
|
||||
elif i > fieldnum:
|
||||
values[field.name] = field.get_min(dateval)
|
||||
i += 1
|
||||
else:
|
||||
value = field.get_value(dateval)
|
||||
maxval = field.get_max(dateval)
|
||||
if value == maxval:
|
||||
fieldnum -= 1
|
||||
i -= 1
|
||||
else:
|
||||
values[field.name] = value + 1
|
||||
i += 1
|
||||
|
||||
return datetime(**values), fieldnum
|
||||
|
||||
def _set_field_value(self, dateval, fieldnum, new_value):
|
||||
values = {}
|
||||
for i, field in enumerate(self.fields):
|
||||
if field.REAL:
|
||||
if i < fieldnum:
|
||||
values[field.name] = field.get_value(dateval)
|
||||
elif i > fieldnum:
|
||||
values[field.name] = field.get_min(dateval)
|
||||
else:
|
||||
values[field.name] = new_value
|
||||
|
||||
return datetime(**values)
|
||||
|
||||
def get_next_fire_time(self, start_date):
|
||||
if self.start_date:
|
||||
start_date = max(start_date, self.start_date)
|
||||
next_date = datetime_ceil(start_date)
|
||||
fieldnum = 0
|
||||
while 0 <= fieldnum < len(self.fields):
|
||||
field = self.fields[fieldnum]
|
||||
curr_value = field.get_value(next_date)
|
||||
next_value = field.get_next_value(next_date)
|
||||
|
||||
if next_value is None:
|
||||
# No valid value was found
|
||||
next_date, fieldnum = self._increment_field_value(next_date,
|
||||
fieldnum - 1)
|
||||
elif next_value > curr_value:
|
||||
# A valid, but higher than the starting value, was found
|
||||
if field.REAL:
|
||||
next_date = self._set_field_value(next_date, fieldnum,
|
||||
next_value)
|
||||
fieldnum += 1
|
||||
else:
|
||||
next_date, fieldnum = self._increment_field_value(next_date,
|
||||
fieldnum)
|
||||
else:
|
||||
# A valid value was found, no changes necessary
|
||||
fieldnum += 1
|
||||
|
||||
if fieldnum >= 0:
|
||||
return next_date
|
||||
|
||||
def __str__(self):
|
||||
options = ["%s='%s'" % (f.name, str(f)) for f in self.fields
|
||||
if not f.is_default]
|
||||
return 'cron[%s]' % (', '.join(options))
|
||||
|
||||
def __repr__(self):
|
||||
options = ["%s='%s'" % (f.name, str(f)) for f in self.fields
|
||||
if not f.is_default]
|
||||
if self.start_date:
|
||||
options.append("start_date='%s'" % self.start_date.isoformat(' '))
|
||||
return '<%s (%s)>' % (self.__class__.__name__, ', '.join(options))
|
||||
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
This module contains the expressions applicable for CronTrigger's fields.
|
||||
"""
|
||||
|
||||
from calendar import monthrange
|
||||
import re
|
||||
|
||||
from lib.apscheduler.util import asint
|
||||
|
||||
__all__ = ('AllExpression', 'RangeExpression', 'WeekdayRangeExpression',
|
||||
'WeekdayPositionExpression')
|
||||
|
||||
|
||||
WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
||||
|
||||
|
||||
class AllExpression(object):
|
||||
value_re = re.compile(r'\*(?:/(?P<step>\d+))?$')
|
||||
|
||||
def __init__(self, step=None):
|
||||
self.step = asint(step)
|
||||
if self.step == 0:
|
||||
raise ValueError('Increment must be higher than 0')
|
||||
|
||||
def get_next_value(self, date, field):
|
||||
start = field.get_value(date)
|
||||
minval = field.get_min(date)
|
||||
maxval = field.get_max(date)
|
||||
start = max(start, minval)
|
||||
|
||||
if not self.step:
|
||||
next = start
|
||||
else:
|
||||
distance_to_next = (self.step - (start - minval)) % self.step
|
||||
next = start + distance_to_next
|
||||
|
||||
if next <= maxval:
|
||||
return next
|
||||
|
||||
def __str__(self):
|
||||
if self.step:
|
||||
return '*/%d' % self.step
|
||||
return '*'
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__.__name__, self.step)
|
||||
|
||||
|
||||
class RangeExpression(AllExpression):
|
||||
value_re = re.compile(
|
||||
r'(?P<first>\d+)(?:-(?P<last>\d+))?(?:/(?P<step>\d+))?$')
|
||||
|
||||
def __init__(self, first, last=None, step=None):
|
||||
AllExpression.__init__(self, step)
|
||||
first = asint(first)
|
||||
last = asint(last)
|
||||
if last is None and step is None:
|
||||
last = first
|
||||
if last is not None and first > last:
|
||||
raise ValueError('The minimum value in a range must not be '
|
||||
'higher than the maximum')
|
||||
self.first = first
|
||||
self.last = last
|
||||
|
||||
def get_next_value(self, date, field):
|
||||
start = field.get_value(date)
|
||||
minval = field.get_min(date)
|
||||
maxval = field.get_max(date)
|
||||
|
||||
# Apply range limits
|
||||
minval = max(minval, self.first)
|
||||
if self.last is not None:
|
||||
maxval = min(maxval, self.last)
|
||||
start = max(start, minval)
|
||||
|
||||
if not self.step:
|
||||
next = start
|
||||
else:
|
||||
distance_to_next = (self.step - (start - minval)) % self.step
|
||||
next = start + distance_to_next
|
||||
|
||||
if next <= maxval:
|
||||
return next
|
||||
|
||||
def __str__(self):
|
||||
if self.last != self.first and self.last is not None:
|
||||
range = '%d-%d' % (self.first, self.last)
|
||||
else:
|
||||
range = str(self.first)
|
||||
|
||||
if self.step:
|
||||
return '%s/%d' % (range, self.step)
|
||||
return range
|
||||
|
||||
def __repr__(self):
|
||||
args = [str(self.first)]
|
||||
if self.last != self.first and self.last is not None or self.step:
|
||||
args.append(str(self.last))
|
||||
if self.step:
|
||||
args.append(str(self.step))
|
||||
return "%s(%s)" % (self.__class__.__name__, ', '.join(args))
|
||||
|
||||
|
||||
class WeekdayRangeExpression(RangeExpression):
|
||||
value_re = re.compile(r'(?P<first>[a-z]+)(?:-(?P<last>[a-z]+))?',
|
||||
re.IGNORECASE)
|
||||
|
||||
def __init__(self, first, last=None):
|
||||
try:
|
||||
first_num = WEEKDAYS.index(first.lower())
|
||||
except ValueError:
|
||||
raise ValueError('Invalid weekday name "%s"' % first)
|
||||
|
||||
if last:
|
||||
try:
|
||||
last_num = WEEKDAYS.index(last.lower())
|
||||
except ValueError:
|
||||
raise ValueError('Invalid weekday name "%s"' % last)
|
||||
else:
|
||||
last_num = None
|
||||
|
||||
RangeExpression.__init__(self, first_num, last_num)
|
||||
|
||||
def __str__(self):
|
||||
if self.last != self.first and self.last is not None:
|
||||
return '%s-%s' % (WEEKDAYS[self.first], WEEKDAYS[self.last])
|
||||
return WEEKDAYS[self.first]
|
||||
|
||||
def __repr__(self):
|
||||
args = ["'%s'" % WEEKDAYS[self.first]]
|
||||
if self.last != self.first and self.last is not None:
|
||||
args.append("'%s'" % WEEKDAYS[self.last])
|
||||
return "%s(%s)" % (self.__class__.__name__, ', '.join(args))
|
||||
|
||||
|
||||
class WeekdayPositionExpression(AllExpression):
|
||||
options = ['1st', '2nd', '3rd', '4th', '5th', 'last']
|
||||
value_re = re.compile(r'(?P<option_name>%s) +(?P<weekday_name>(?:\d+|\w+))'
|
||||
% '|'.join(options), re.IGNORECASE)
|
||||
|
||||
def __init__(self, option_name, weekday_name):
|
||||
try:
|
||||
self.option_num = self.options.index(option_name.lower())
|
||||
except ValueError:
|
||||
raise ValueError('Invalid weekday position "%s"' % option_name)
|
||||
|
||||
try:
|
||||
self.weekday = WEEKDAYS.index(weekday_name.lower())
|
||||
except ValueError:
|
||||
raise ValueError('Invalid weekday name "%s"' % weekday_name)
|
||||
|
||||
def get_next_value(self, date, field):
|
||||
# Figure out the weekday of the month's first day and the number
|
||||
# of days in that month
|
||||
first_day_wday, last_day = monthrange(date.year, date.month)
|
||||
|
||||
# Calculate which day of the month is the first of the target weekdays
|
||||
first_hit_day = self.weekday - first_day_wday + 1
|
||||
if first_hit_day <= 0:
|
||||
first_hit_day += 7
|
||||
|
||||
# Calculate what day of the month the target weekday would be
|
||||
if self.option_num < 5:
|
||||
target_day = first_hit_day + self.option_num * 7
|
||||
else:
|
||||
target_day = first_hit_day + ((last_day - first_hit_day) / 7) * 7
|
||||
|
||||
if target_day <= last_day and target_day >= date.day:
|
||||
return target_day
|
||||
|
||||
def __str__(self):
|
||||
return '%s %s' % (self.options[self.option_num],
|
||||
WEEKDAYS[self.weekday])
|
||||
|
||||
def __repr__(self):
|
||||
return "%s('%s', '%s')" % (self.__class__.__name__,
|
||||
self.options[self.option_num],
|
||||
WEEKDAYS[self.weekday])
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Fields represent CronTrigger options which map to :class:`~datetime.datetime`
|
||||
fields.
|
||||
"""
|
||||
|
||||
from calendar import monthrange
|
||||
|
||||
from lib.apscheduler.triggers.cron.expressions import *
|
||||
|
||||
__all__ = ('MIN_VALUES', 'MAX_VALUES', 'DEFAULT_VALUES', 'BaseField',
|
||||
'WeekField', 'DayOfMonthField', 'DayOfWeekField')
|
||||
|
||||
|
||||
MIN_VALUES = {'year': 1970, 'month': 1, 'day': 1, 'week': 1,
|
||||
'day_of_week': 0, 'hour': 0, 'minute': 0, 'second': 0}
|
||||
MAX_VALUES = {'year': 2 ** 63, 'month': 12, 'day:': 31, 'week': 53,
|
||||
'day_of_week': 6, 'hour': 23, 'minute': 59, 'second': 59}
|
||||
DEFAULT_VALUES = {'year': '*', 'month': 1, 'day': 1, 'week': '*',
|
||||
'day_of_week': '*', 'hour': 0, 'minute': 0, 'second': 0}
|
||||
|
||||
|
||||
class BaseField(object):
|
||||
REAL = True
|
||||
COMPILERS = [AllExpression, RangeExpression]
|
||||
|
||||
def __init__(self, name, exprs, is_default=False):
|
||||
self.name = name
|
||||
self.is_default = is_default
|
||||
self.compile_expressions(exprs)
|
||||
|
||||
def get_min(self, dateval):
|
||||
return MIN_VALUES[self.name]
|
||||
|
||||
def get_max(self, dateval):
|
||||
return MAX_VALUES[self.name]
|
||||
|
||||
def get_value(self, dateval):
|
||||
return getattr(dateval, self.name)
|
||||
|
||||
def get_next_value(self, dateval):
|
||||
smallest = None
|
||||
for expr in self.expressions:
|
||||
value = expr.get_next_value(dateval, self)
|
||||
if smallest is None or (value is not None and value < smallest):
|
||||
smallest = value
|
||||
|
||||
return smallest
|
||||
|
||||
def compile_expressions(self, exprs):
|
||||
self.expressions = []
|
||||
|
||||
# Split a comma-separated expression list, if any
|
||||
exprs = str(exprs).strip()
|
||||
if ',' in exprs:
|
||||
for expr in exprs.split(','):
|
||||
self.compile_expression(expr)
|
||||
else:
|
||||
self.compile_expression(exprs)
|
||||
|
||||
def compile_expression(self, expr):
|
||||
for compiler in self.COMPILERS:
|
||||
match = compiler.value_re.match(expr)
|
||||
if match:
|
||||
compiled_expr = compiler(**match.groupdict())
|
||||
self.expressions.append(compiled_expr)
|
||||
return
|
||||
|
||||
raise ValueError('Unrecognized expression "%s" for field "%s"' %
|
||||
(expr, self.name))
|
||||
|
||||
def __str__(self):
|
||||
expr_strings = (str(e) for e in self.expressions)
|
||||
return ','.join(expr_strings)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s('%s', '%s')" % (self.__class__.__name__, self.name,
|
||||
str(self))
|
||||
|
||||
|
||||
class WeekField(BaseField):
|
||||
REAL = False
|
||||
|
||||
def get_value(self, dateval):
|
||||
return dateval.isocalendar()[1]
|
||||
|
||||
|
||||
class DayOfMonthField(BaseField):
|
||||
COMPILERS = BaseField.COMPILERS + [WeekdayPositionExpression]
|
||||
|
||||
def get_max(self, dateval):
|
||||
return monthrange(dateval.year, dateval.month)[1]
|
||||
|
||||
|
||||
class DayOfWeekField(BaseField):
|
||||
REAL = False
|
||||
COMPILERS = BaseField.COMPILERS + [WeekdayRangeExpression]
|
||||
|
||||
def get_value(self, dateval):
|
||||
return dateval.weekday()
|
||||
@@ -0,0 +1,39 @@
|
||||
from datetime import datetime, timedelta
|
||||
from math import ceil
|
||||
|
||||
from lib.apscheduler.util import convert_to_datetime, timedelta_seconds
|
||||
|
||||
|
||||
class IntervalTrigger(object):
|
||||
def __init__(self, interval, start_date=None):
|
||||
if not isinstance(interval, timedelta):
|
||||
raise TypeError('interval must be a timedelta')
|
||||
if start_date:
|
||||
start_date = convert_to_datetime(start_date)
|
||||
|
||||
self.interval = interval
|
||||
self.interval_length = timedelta_seconds(self.interval)
|
||||
if self.interval_length == 0:
|
||||
self.interval = timedelta(seconds=1)
|
||||
self.interval_length = 1
|
||||
|
||||
if start_date is None:
|
||||
self.start_date = datetime.now() + self.interval
|
||||
else:
|
||||
self.start_date = convert_to_datetime(start_date)
|
||||
|
||||
def get_next_fire_time(self, start_date):
|
||||
if start_date < self.start_date:
|
||||
return self.start_date
|
||||
|
||||
timediff_seconds = timedelta_seconds(start_date - self.start_date)
|
||||
next_interval_num = int(ceil(timediff_seconds / self.interval_length))
|
||||
return self.start_date + self.interval * next_interval_num
|
||||
|
||||
def __str__(self):
|
||||
return 'interval[%s]' % str(self.interval)
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s (interval=%s, start_date=%s)>" % (
|
||||
self.__class__.__name__, repr(self.interval),
|
||||
repr(self.start_date))
|
||||
@@ -0,0 +1,17 @@
|
||||
from lib.apscheduler.util import convert_to_datetime
|
||||
|
||||
|
||||
class SimpleTrigger(object):
|
||||
def __init__(self, run_date):
|
||||
self.run_date = convert_to_datetime(run_date)
|
||||
|
||||
def get_next_fire_time(self, start_date):
|
||||
if self.run_date >= start_date:
|
||||
return self.run_date
|
||||
|
||||
def __str__(self):
|
||||
return 'date[%s]' % str(self.run_date)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s (run_date=%s)>' % (
|
||||
self.__class__.__name__, repr(self.run_date))
|
||||
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
This module contains several handy functions primarily meant for internal use.
|
||||
"""
|
||||
|
||||
from datetime import date, datetime, timedelta
|
||||
from time import mktime
|
||||
import re
|
||||
import sys
|
||||
|
||||
__all__ = ('asint', 'asbool', 'convert_to_datetime', 'timedelta_seconds',
|
||||
'time_difference', 'datetime_ceil', 'combine_opts',
|
||||
'get_callable_name', 'obj_to_ref', 'ref_to_obj', 'maybe_ref',
|
||||
'to_unicode', 'iteritems', 'itervalues', 'xrange')
|
||||
|
||||
|
||||
def asint(text):
|
||||
"""
|
||||
Safely converts a string to an integer, returning None if the string
|
||||
is None.
|
||||
|
||||
:type text: str
|
||||
:rtype: int
|
||||
"""
|
||||
if text is not None:
|
||||
return int(text)
|
||||
|
||||
|
||||
def asbool(obj):
|
||||
"""
|
||||
Interprets an object as a boolean value.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
if isinstance(obj, str):
|
||||
obj = obj.strip().lower()
|
||||
if obj in ('true', 'yes', 'on', 'y', 't', '1'):
|
||||
return True
|
||||
if obj in ('false', 'no', 'off', 'n', 'f', '0'):
|
||||
return False
|
||||
raise ValueError('Unable to interpret value "%s" as boolean' % obj)
|
||||
return bool(obj)
|
||||
|
||||
|
||||
_DATE_REGEX = re.compile(
|
||||
r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
|
||||
r'(?: (?P<hour>\d{1,2}):(?P<minute>\d{1,2}):(?P<second>\d{1,2})'
|
||||
r'(?:\.(?P<microsecond>\d{1,6}))?)?')
|
||||
|
||||
|
||||
def convert_to_datetime(input):
|
||||
"""
|
||||
Converts the given object to a datetime object, if possible.
|
||||
If an actual datetime object is passed, it is returned unmodified.
|
||||
If the input is a string, it is parsed as a datetime.
|
||||
|
||||
Date strings are accepted in three different forms: date only (Y-m-d),
|
||||
date with time (Y-m-d H:M:S) or with date+time with microseconds
|
||||
(Y-m-d H:M:S.micro).
|
||||
|
||||
:rtype: datetime
|
||||
"""
|
||||
if isinstance(input, datetime):
|
||||
return input
|
||||
elif isinstance(input, date):
|
||||
return datetime.fromordinal(input.toordinal())
|
||||
elif isinstance(input, str):
|
||||
m = _DATE_REGEX.match(input)
|
||||
if not m:
|
||||
raise ValueError('Invalid date string')
|
||||
values = [(k, int(v or 0)) for k, v in m.groupdict().items()]
|
||||
values = dict(values)
|
||||
return datetime(**values)
|
||||
raise TypeError('Unsupported input type: %s' % type(input))
|
||||
|
||||
|
||||
def timedelta_seconds(delta):
|
||||
"""
|
||||
Converts the given timedelta to seconds.
|
||||
|
||||
:type delta: timedelta
|
||||
:rtype: float
|
||||
"""
|
||||
return delta.days * 24 * 60 * 60 + delta.seconds + \
|
||||
delta.microseconds / 1000000.0
|
||||
|
||||
|
||||
def time_difference(date1, date2):
|
||||
"""
|
||||
Returns the time difference in seconds between the given two
|
||||
datetime objects. The difference is calculated as: date1 - date2.
|
||||
|
||||
:param date1: the later datetime
|
||||
:type date1: datetime
|
||||
:param date2: the earlier datetime
|
||||
:type date2: datetime
|
||||
:rtype: float
|
||||
"""
|
||||
later = mktime(date1.timetuple()) + date1.microsecond / 1000000.0
|
||||
earlier = mktime(date2.timetuple()) + date2.microsecond / 1000000.0
|
||||
return later - earlier
|
||||
|
||||
|
||||
def datetime_ceil(dateval):
|
||||
"""
|
||||
Rounds the given datetime object upwards.
|
||||
|
||||
:type dateval: datetime
|
||||
"""
|
||||
if dateval.microsecond > 0:
|
||||
return dateval + timedelta(seconds=1,
|
||||
microseconds=-dateval.microsecond)
|
||||
return dateval
|
||||
|
||||
|
||||
def combine_opts(global_config, prefix, local_config={}):
|
||||
"""
|
||||
Returns a subdictionary from keys and values of ``global_config`` where
|
||||
the key starts with the given prefix, combined with options from
|
||||
local_config. The keys in the subdictionary have the prefix removed.
|
||||
|
||||
:type global_config: dict
|
||||
:type prefix: str
|
||||
:type local_config: dict
|
||||
:rtype: dict
|
||||
"""
|
||||
prefixlen = len(prefix)
|
||||
subconf = {}
|
||||
for key, value in global_config.items():
|
||||
if key.startswith(prefix):
|
||||
key = key[prefixlen:]
|
||||
subconf[key] = value
|
||||
subconf.update(local_config)
|
||||
return subconf
|
||||
|
||||
|
||||
def get_callable_name(func):
|
||||
"""
|
||||
Returns the best available display name for the given function/callable.
|
||||
"""
|
||||
name = func.__module__
|
||||
if hasattr(func, '__self__') and func.__self__:
|
||||
name += '.' + func.__self__.__name__
|
||||
elif hasattr(func, 'im_self') and func.im_self: # py2.4, 2.5
|
||||
name += '.' + func.im_self.__name__
|
||||
if hasattr(func, '__name__'):
|
||||
name += '.' + func.__name__
|
||||
return name
|
||||
|
||||
|
||||
def obj_to_ref(obj):
|
||||
"""
|
||||
Returns the path to the given object.
|
||||
"""
|
||||
ref = '%s:%s' % (obj.__module__, obj.__name__)
|
||||
try:
|
||||
obj2 = ref_to_obj(ref)
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if obj2 == obj:
|
||||
return ref
|
||||
|
||||
raise ValueError('Only module level objects are supported')
|
||||
|
||||
|
||||
def ref_to_obj(ref):
|
||||
"""
|
||||
Returns the object pointed to by ``ref``.
|
||||
"""
|
||||
modulename, rest = ref.split(':', 1)
|
||||
obj = __import__(modulename)
|
||||
for name in modulename.split('.')[1:] + rest.split('.'):
|
||||
obj = getattr(obj, name)
|
||||
return obj
|
||||
|
||||
|
||||
def maybe_ref(ref):
|
||||
"""
|
||||
Returns the object that the given reference points to, if it is indeed
|
||||
a reference. If it is not a reference, the object is returned as-is.
|
||||
"""
|
||||
if not isinstance(ref, str):
|
||||
return ref
|
||||
return ref_to_obj(ref)
|
||||
|
||||
|
||||
def to_unicode(string, encoding='ascii'):
|
||||
"""
|
||||
Safely converts a string to a unicode representation on any
|
||||
Python version.
|
||||
"""
|
||||
if hasattr(string, 'decode'):
|
||||
return string.decode(encoding, 'ignore')
|
||||
return string
|
||||
|
||||
|
||||
if sys.version_info < (3, 0): # pragma: nocover
|
||||
iteritems = lambda d: d.iteritems()
|
||||
itervalues = lambda d: d.itervalues()
|
||||
xrange = xrange
|
||||
else: # pragma: nocover
|
||||
iteritems = lambda d: d.items()
|
||||
itervalues = lambda d: d.values()
|
||||
xrange = range
|
||||
+2386
File diff suppressed because it is too large
Load Diff
+2468
File diff suppressed because it is too large
Load Diff
+3909
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
"""A collection of classes for MusicBrainz.
|
||||
|
||||
To get started quickly, have a look at L{webservice.Query} and the examples
|
||||
there. The source distribution also contains example code you might find
|
||||
interesting.
|
||||
|
||||
This package contains the following modules:
|
||||
|
||||
1. L{model}: The MusicBrainz domain model, containing classes like
|
||||
L{Artist <model.Artist>}, L{Release <model.Release>}, or
|
||||
L{Track <model.Track>}
|
||||
|
||||
2. L{webservice}: An interface to the MusicBrainz XML web service.
|
||||
|
||||
3. L{wsxml}: A parser for the web service XML format (MMD).
|
||||
|
||||
4. L{disc}: Functions for creating and submitting DiscIDs.
|
||||
|
||||
5. L{utils}: Utilities for working with URIs and other commonly needed tools.
|
||||
|
||||
@author: Matthias Friedrich <matt@mafr.de>
|
||||
"""
|
||||
__revision__ = '$Id: __init__.py 12974 2011-05-01 08:43:54Z luks $'
|
||||
__version__ = '0.7.3'
|
||||
|
||||
# EOF
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Support data for the musicbrainz2 package.
|
||||
|
||||
This package is I{not} part of the public API, it has been added to work
|
||||
around shortcomings in python and may thus be removed at any time.
|
||||
|
||||
Please use the L{musicbrainz2.utils} module instead.
|
||||
"""
|
||||
__revision__ = '$Id: __init__.py 7386 2006-04-30 11:12:55Z matt $'
|
||||
|
||||
# EOF
|
||||
@@ -0,0 +1,253 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__revision__ = '$Id: countrynames.py 7386 2006-04-30 11:12:55Z matt $'
|
||||
|
||||
countryNames = {
|
||||
u'BD': u'Bangladesh',
|
||||
u'BE': u'Belgium',
|
||||
u'BF': u'Burkina Faso',
|
||||
u'BG': u'Bulgaria',
|
||||
u'BB': u'Barbados',
|
||||
u'WF': u'Wallis and Futuna Islands',
|
||||
u'BM': u'Bermuda',
|
||||
u'BN': u'Brunei Darussalam',
|
||||
u'BO': u'Bolivia',
|
||||
u'BH': u'Bahrain',
|
||||
u'BI': u'Burundi',
|
||||
u'BJ': u'Benin',
|
||||
u'BT': u'Bhutan',
|
||||
u'JM': u'Jamaica',
|
||||
u'BV': u'Bouvet Island',
|
||||
u'BW': u'Botswana',
|
||||
u'WS': u'Samoa',
|
||||
u'BR': u'Brazil',
|
||||
u'BS': u'Bahamas',
|
||||
u'BY': u'Belarus',
|
||||
u'BZ': u'Belize',
|
||||
u'RU': u'Russian Federation',
|
||||
u'RW': u'Rwanda',
|
||||
u'RE': u'Reunion',
|
||||
u'TM': u'Turkmenistan',
|
||||
u'TJ': u'Tajikistan',
|
||||
u'RO': u'Romania',
|
||||
u'TK': u'Tokelau',
|
||||
u'GW': u'Guinea-Bissau',
|
||||
u'GU': u'Guam',
|
||||
u'GT': u'Guatemala',
|
||||
u'GR': u'Greece',
|
||||
u'GQ': u'Equatorial Guinea',
|
||||
u'GP': u'Guadeloupe',
|
||||
u'JP': u'Japan',
|
||||
u'GY': u'Guyana',
|
||||
u'GF': u'French Guiana',
|
||||
u'GE': u'Georgia',
|
||||
u'GD': u'Grenada',
|
||||
u'GB': u'United Kingdom',
|
||||
u'GA': u'Gabon',
|
||||
u'SV': u'El Salvador',
|
||||
u'GN': u'Guinea',
|
||||
u'GM': u'Gambia',
|
||||
u'GL': u'Greenland',
|
||||
u'GI': u'Gibraltar',
|
||||
u'GH': u'Ghana',
|
||||
u'OM': u'Oman',
|
||||
u'TN': u'Tunisia',
|
||||
u'JO': u'Jordan',
|
||||
u'HT': u'Haiti',
|
||||
u'HU': u'Hungary',
|
||||
u'HK': u'Hong Kong',
|
||||
u'HN': u'Honduras',
|
||||
u'HM': u'Heard and Mc Donald Islands',
|
||||
u'VE': u'Venezuela',
|
||||
u'PR': u'Puerto Rico',
|
||||
u'PW': u'Palau',
|
||||
u'PT': u'Portugal',
|
||||
u'SJ': u'Svalbard and Jan Mayen Islands',
|
||||
u'PY': u'Paraguay',
|
||||
u'IQ': u'Iraq',
|
||||
u'PA': u'Panama',
|
||||
u'PF': u'French Polynesia',
|
||||
u'PG': u'Papua New Guinea',
|
||||
u'PE': u'Peru',
|
||||
u'PK': u'Pakistan',
|
||||
u'PH': u'Philippines',
|
||||
u'PN': u'Pitcairn',
|
||||
u'PL': u'Poland',
|
||||
u'PM': u'St. Pierre and Miquelon',
|
||||
u'ZM': u'Zambia',
|
||||
u'EH': u'Western Sahara',
|
||||
u'EE': u'Estonia',
|
||||
u'EG': u'Egypt',
|
||||
u'ZA': u'South Africa',
|
||||
u'EC': u'Ecuador',
|
||||
u'IT': u'Italy',
|
||||
u'VN': u'Viet Nam',
|
||||
u'SB': u'Solomon Islands',
|
||||
u'ET': u'Ethiopia',
|
||||
u'SO': u'Somalia',
|
||||
u'ZW': u'Zimbabwe',
|
||||
u'SA': u'Saudi Arabia',
|
||||
u'ES': u'Spain',
|
||||
u'ER': u'Eritrea',
|
||||
u'MD': u'Moldova, Republic of',
|
||||
u'MG': u'Madagascar',
|
||||
u'MA': u'Morocco',
|
||||
u'MC': u'Monaco',
|
||||
u'UZ': u'Uzbekistan',
|
||||
u'MM': u'Myanmar',
|
||||
u'ML': u'Mali',
|
||||
u'MO': u'Macau',
|
||||
u'MN': u'Mongolia',
|
||||
u'MH': u'Marshall Islands',
|
||||
u'MK': u'Macedonia, The Former Yugoslav Republic of',
|
||||
u'MU': u'Mauritius',
|
||||
u'MT': u'Malta',
|
||||
u'MW': u'Malawi',
|
||||
u'MV': u'Maldives',
|
||||
u'MQ': u'Martinique',
|
||||
u'MP': u'Northern Mariana Islands',
|
||||
u'MS': u'Montserrat',
|
||||
u'MR': u'Mauritania',
|
||||
u'UG': u'Uganda',
|
||||
u'MY': u'Malaysia',
|
||||
u'MX': u'Mexico',
|
||||
u'IL': u'Israel',
|
||||
u'FR': u'France',
|
||||
u'IO': u'British Indian Ocean Territory',
|
||||
u'SH': u'St. Helena',
|
||||
u'FI': u'Finland',
|
||||
u'FJ': u'Fiji',
|
||||
u'FK': u'Falkland Islands (Malvinas)',
|
||||
u'FM': u'Micronesia, Federated States of',
|
||||
u'FO': u'Faroe Islands',
|
||||
u'NI': u'Nicaragua',
|
||||
u'NL': u'Netherlands',
|
||||
u'NO': u'Norway',
|
||||
u'NA': u'Namibia',
|
||||
u'VU': u'Vanuatu',
|
||||
u'NC': u'New Caledonia',
|
||||
u'NE': u'Niger',
|
||||
u'NF': u'Norfolk Island',
|
||||
u'NG': u'Nigeria',
|
||||
u'NZ': u'New Zealand',
|
||||
u'ZR': u'Zaire',
|
||||
u'NP': u'Nepal',
|
||||
u'NR': u'Nauru',
|
||||
u'NU': u'Niue',
|
||||
u'CK': u'Cook Islands',
|
||||
u'CI': u'Cote d\'Ivoire',
|
||||
u'CH': u'Switzerland',
|
||||
u'CO': u'Colombia',
|
||||
u'CN': u'China',
|
||||
u'CM': u'Cameroon',
|
||||
u'CL': u'Chile',
|
||||
u'CC': u'Cocos (Keeling) Islands',
|
||||
u'CA': u'Canada',
|
||||
u'CG': u'Congo',
|
||||
u'CF': u'Central African Republic',
|
||||
u'CZ': u'Czech Republic',
|
||||
u'CY': u'Cyprus',
|
||||
u'CX': u'Christmas Island',
|
||||
u'CR': u'Costa Rica',
|
||||
u'CV': u'Cape Verde',
|
||||
u'CU': u'Cuba',
|
||||
u'SZ': u'Swaziland',
|
||||
u'SY': u'Syrian Arab Republic',
|
||||
u'KG': u'Kyrgyzstan',
|
||||
u'KE': u'Kenya',
|
||||
u'SR': u'Suriname',
|
||||
u'KI': u'Kiribati',
|
||||
u'KH': u'Cambodia',
|
||||
u'KN': u'Saint Kitts and Nevis',
|
||||
u'KM': u'Comoros',
|
||||
u'ST': u'Sao Tome and Principe',
|
||||
u'SI': u'Slovenia',
|
||||
u'KW': u'Kuwait',
|
||||
u'SN': u'Senegal',
|
||||
u'SM': u'San Marino',
|
||||
u'SL': u'Sierra Leone',
|
||||
u'SC': u'Seychelles',
|
||||
u'KZ': u'Kazakhstan',
|
||||
u'KY': u'Cayman Islands',
|
||||
u'SG': u'Singapore',
|
||||
u'SE': u'Sweden',
|
||||
u'SD': u'Sudan',
|
||||
u'DO': u'Dominican Republic',
|
||||
u'DM': u'Dominica',
|
||||
u'DJ': u'Djibouti',
|
||||
u'DK': u'Denmark',
|
||||
u'VG': u'Virgin Islands (British)',
|
||||
u'DE': u'Germany',
|
||||
u'YE': u'Yemen',
|
||||
u'DZ': u'Algeria',
|
||||
u'US': u'United States',
|
||||
u'UY': u'Uruguay',
|
||||
u'YT': u'Mayotte',
|
||||
u'UM': u'United States Minor Outlying Islands',
|
||||
u'LB': u'Lebanon',
|
||||
u'LC': u'Saint Lucia',
|
||||
u'LA': u'Lao People\'s Democratic Republic',
|
||||
u'TV': u'Tuvalu',
|
||||
u'TW': u'Taiwan',
|
||||
u'TT': u'Trinidad and Tobago',
|
||||
u'TR': u'Turkey',
|
||||
u'LK': u'Sri Lanka',
|
||||
u'LI': u'Liechtenstein',
|
||||
u'LV': u'Latvia',
|
||||
u'TO': u'Tonga',
|
||||
u'LT': u'Lithuania',
|
||||
u'LU': u'Luxembourg',
|
||||
u'LR': u'Liberia',
|
||||
u'LS': u'Lesotho',
|
||||
u'TH': u'Thailand',
|
||||
u'TF': u'French Southern Territories',
|
||||
u'TG': u'Togo',
|
||||
u'TD': u'Chad',
|
||||
u'TC': u'Turks and Caicos Islands',
|
||||
u'LY': u'Libyan Arab Jamahiriya',
|
||||
u'VA': u'Vatican City State (Holy See)',
|
||||
u'VC': u'Saint Vincent and The Grenadines',
|
||||
u'AE': u'United Arab Emirates',
|
||||
u'AD': u'Andorra',
|
||||
u'AG': u'Antigua and Barbuda',
|
||||
u'AF': u'Afghanistan',
|
||||
u'AI': u'Anguilla',
|
||||
u'VI': u'Virgin Islands (U.S.)',
|
||||
u'IS': u'Iceland',
|
||||
u'IR': u'Iran (Islamic Republic of)',
|
||||
u'AM': u'Armenia',
|
||||
u'AL': u'Albania',
|
||||
u'AO': u'Angola',
|
||||
u'AN': u'Netherlands Antilles',
|
||||
u'AQ': u'Antarctica',
|
||||
u'AS': u'American Samoa',
|
||||
u'AR': u'Argentina',
|
||||
u'AU': u'Australia',
|
||||
u'AT': u'Austria',
|
||||
u'AW': u'Aruba',
|
||||
u'IN': u'India',
|
||||
u'TZ': u'Tanzania, United Republic of',
|
||||
u'AZ': u'Azerbaijan',
|
||||
u'IE': u'Ireland',
|
||||
u'ID': u'Indonesia',
|
||||
u'UA': u'Ukraine',
|
||||
u'QA': u'Qatar',
|
||||
u'MZ': u'Mozambique',
|
||||
u'BA': u'Bosnia and Herzegovina',
|
||||
u'CD': u'Congo, The Democratic Republic of the',
|
||||
u'CS': u'Serbia and Montenegro',
|
||||
u'HR': u'Croatia',
|
||||
u'KP': u'Korea (North), Democratic People\'s Republic of',
|
||||
u'KR': u'Korea (South), Republic of',
|
||||
u'SK': u'Slovakia',
|
||||
u'SU': u'Soviet Union (historical, 1922-1991)',
|
||||
u'TL': u'East Timor',
|
||||
u'XC': u'Czechoslovakia (historical, 1918-1992)',
|
||||
u'XE': u'Europe',
|
||||
u'XG': u'East Germany (historical, 1949-1990)',
|
||||
u'XU': u'[Unknown Country]',
|
||||
u'XW': u'[Worldwide]',
|
||||
u'YU': u'Yugoslavia (historical, 1918-1992)',
|
||||
}
|
||||
|
||||
# EOF
|
||||
@@ -0,0 +1,400 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__revision__ = '$Id: languagenames.py 8725 2006-12-17 22:39:07Z luks $'
|
||||
|
||||
languageNames = {
|
||||
u'ART': u'Artificial (Other)',
|
||||
u'ROH': u'Raeto-Romance',
|
||||
u'SCO': u'Scots',
|
||||
u'SCN': u'Sicilian',
|
||||
u'ROM': u'Romany',
|
||||
u'RON': u'Romanian',
|
||||
u'OSS': u'Ossetian; Ossetic',
|
||||
u'ALE': u'Aleut',
|
||||
u'MNI': u'Manipuri',
|
||||
u'NWC': u'Classical Newari; Old Newari; Classical Nepal Bhasa',
|
||||
u'OSA': u'Osage',
|
||||
u'MNC': u'Manchu',
|
||||
u'MWR': u'Marwari',
|
||||
u'VEN': u'Venda',
|
||||
u'MWL': u'Mirandese',
|
||||
u'FAS': u'Persian',
|
||||
u'FAT': u'Fanti',
|
||||
u'FAN': u'Fang',
|
||||
u'FAO': u'Faroese',
|
||||
u'DIN': u'Dinka',
|
||||
u'HYE': u'Armenian',
|
||||
u'DSB': u'Lower Sorbian',
|
||||
u'CAR': u'Carib',
|
||||
u'DIV': u'Divehi',
|
||||
u'TEL': u'Telugu',
|
||||
u'TEM': u'Timne',
|
||||
u'NBL': u'Ndebele, South; South Ndebele',
|
||||
u'TER': u'Tereno',
|
||||
u'TET': u'Tetum',
|
||||
u'SUN': u'Sundanese',
|
||||
u'KUT': u'Kutenai',
|
||||
u'SUK': u'Sukuma',
|
||||
u'KUR': u'Kurdish',
|
||||
u'KUM': u'Kumyk',
|
||||
u'SUS': u'Susu',
|
||||
u'NEW': u'Newari; Nepal Bhasa',
|
||||
u'KUA': u'Kuanyama; Kwanyama',
|
||||
u'MEN': u'Mende',
|
||||
u'LEZ': u'Lezghian',
|
||||
u'GLA': u'Gaelic; Scottish Gaelic',
|
||||
u'BOS': u'Bosnian',
|
||||
u'GLE': u'Irish',
|
||||
u'EKA': u'Ekajuk',
|
||||
u'GLG': u'Gallegan',
|
||||
u'AKA': u'Akan',
|
||||
u'BOD': u'Tibetan',
|
||||
u'GLV': u'Manx',
|
||||
u'JRB': u'Judeo-Arabic',
|
||||
u'VIE': u'Vietnamese',
|
||||
u'IPK': u'Inupiaq',
|
||||
u'UZB': u'Uzbek',
|
||||
u'BRE': u'Breton',
|
||||
u'BRA': u'Braj',
|
||||
u'AYM': u'Aymara',
|
||||
u'CHA': u'Chamorro',
|
||||
u'CHB': u'Chibcha',
|
||||
u'CHE': u'Chechen',
|
||||
u'CHG': u'Chagatai',
|
||||
u'CHK': u'Chuukese',
|
||||
u'CHM': u'Mari',
|
||||
u'CHN': u'Chinook jargon',
|
||||
u'CHO': u'Choctaw',
|
||||
u'CHP': u'Chipewyan',
|
||||
u'CHR': u'Cherokee',
|
||||
u'CHU': u'Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic',
|
||||
u'CHV': u'Chuvash',
|
||||
u'CHY': u'Cheyenne',
|
||||
u'MSA': u'Malay',
|
||||
u'III': u'Sichuan Yi',
|
||||
u'ACE': u'Achinese',
|
||||
u'IBO': u'Igbo',
|
||||
u'IBA': u'Iban',
|
||||
u'XHO': u'Xhosa',
|
||||
u'DEU': u'German',
|
||||
u'CAT': u'Catalan; Valencian',
|
||||
u'DEL': u'Delaware',
|
||||
u'DEN': u'Slave (Athapascan)',
|
||||
u'CAD': u'Caddo',
|
||||
u'TAT': u'Tatar',
|
||||
u'RAJ': u'Rajasthani',
|
||||
u'SPA': u'Spanish; Castilian',
|
||||
u'TAM': u'Tamil',
|
||||
u'TAH': u'Tahitian',
|
||||
u'AFH': u'Afrihili',
|
||||
u'ENG': u'English',
|
||||
u'CSB': u'Kashubian',
|
||||
u'NYN': u'Nyankole',
|
||||
u'NYO': u'Nyoro',
|
||||
u'SID': u'Sidamo',
|
||||
u'NYA': u'Chichewa; Chewa; Nyanja',
|
||||
u'SIN': u'Sinhala; Sinhalese',
|
||||
u'AFR': u'Afrikaans',
|
||||
u'LAM': u'Lamba',
|
||||
u'SND': u'Sindhi',
|
||||
u'MAR': u'Marathi',
|
||||
u'LAH': u'Lahnda',
|
||||
u'NYM': u'Nyamwezi',
|
||||
u'SNA': u'Shona',
|
||||
u'LAD': u'Ladino',
|
||||
u'SNK': u'Soninke',
|
||||
u'MAD': u'Madurese',
|
||||
u'MAG': u'Magahi',
|
||||
u'MAI': u'Maithili',
|
||||
u'MAH': u'Marshallese',
|
||||
u'LAV': u'Latvian',
|
||||
u'MAL': u'Malayalam',
|
||||
u'MAN': u'Mandingo',
|
||||
u'ZND': u'Zande',
|
||||
u'ZEN': u'Zenaga',
|
||||
u'KBD': u'Kabardian',
|
||||
u'ITA': u'Italian',
|
||||
u'VAI': u'Vai',
|
||||
u'TSN': u'Tswana',
|
||||
u'TSO': u'Tsonga',
|
||||
u'TSI': u'Tsimshian',
|
||||
u'BYN': u'Blin; Bilin',
|
||||
u'FIJ': u'Fijian',
|
||||
u'FIN': u'Finnish',
|
||||
u'EUS': u'Basque',
|
||||
u'CEB': u'Cebuano',
|
||||
u'DAN': u'Danish',
|
||||
u'NOG': u'Nogai',
|
||||
u'NOB': u'Norwegian Bokmål; Bokmål, Norwegian',
|
||||
u'DAK': u'Dakota',
|
||||
u'CES': u'Czech',
|
||||
u'DAR': u'Dargwa',
|
||||
u'DAY': u'Dayak',
|
||||
u'NOR': u'Norwegian',
|
||||
u'KPE': u'Kpelle',
|
||||
u'GUJ': u'Gujarati',
|
||||
u'MDF': u'Moksha',
|
||||
u'MAS': u'Masai',
|
||||
u'LAO': u'Lao',
|
||||
u'MDR': u'Mandar',
|
||||
u'GON': u'Gondi',
|
||||
u'SMS': u'Skolt Sami',
|
||||
u'SMO': u'Samoan',
|
||||
u'SMN': u'Inari Sami',
|
||||
u'SMJ': u'Lule Sami',
|
||||
u'GOT': u'Gothic',
|
||||
u'SME': u'Northern Sami',
|
||||
u'BLA': u'Siksika',
|
||||
u'SMA': u'Southern Sami',
|
||||
u'GOR': u'Gorontalo',
|
||||
u'AST': u'Asturian; Bable',
|
||||
u'ORM': u'Oromo',
|
||||
u'QUE': u'Quechua',
|
||||
u'ORI': u'Oriya',
|
||||
u'CRH': u'Crimean Tatar; Crimean Turkish',
|
||||
u'ASM': u'Assamese',
|
||||
u'PUS': u'Pushto',
|
||||
u'DGR': u'Dogrib',
|
||||
u'LTZ': u'Luxembourgish; Letzeburgesch',
|
||||
u'NDO': u'Ndonga',
|
||||
u'GEZ': u'Geez',
|
||||
u'ISL': u'Icelandic',
|
||||
u'LAT': u'Latin',
|
||||
u'MAK': u'Makasar',
|
||||
u'ZAP': u'Zapotec',
|
||||
u'YID': u'Yiddish',
|
||||
u'KOK': u'Konkani',
|
||||
u'KOM': u'Komi',
|
||||
u'KON': u'Kongo',
|
||||
u'UKR': u'Ukrainian',
|
||||
u'TON': u'Tonga (Tonga Islands)',
|
||||
u'KOS': u'Kosraean',
|
||||
u'KOR': u'Korean',
|
||||
u'TOG': u'Tonga (Nyasa)',
|
||||
u'HUN': u'Hungarian',
|
||||
u'HUP': u'Hupa',
|
||||
u'CYM': u'Welsh',
|
||||
u'UDM': u'Udmurt',
|
||||
u'BEJ': u'Beja',
|
||||
u'BEN': u'Bengali',
|
||||
u'BEL': u'Belarusian',
|
||||
u'BEM': u'Bemba',
|
||||
u'AAR': u'Afar',
|
||||
u'NZI': u'Nzima',
|
||||
u'SAH': u'Yakut',
|
||||
u'SAN': u'Sanskrit',
|
||||
u'SAM': u'Samaritan Aramaic',
|
||||
u'SAG': u'Sango',
|
||||
u'SAD': u'Sandawe',
|
||||
u'RAR': u'Rarotongan',
|
||||
u'RAP': u'Rapanui',
|
||||
u'SAS': u'Sasak',
|
||||
u'SAT': u'Santali',
|
||||
u'MIN': u'Minangkabau',
|
||||
u'LIM': u'Limburgan; Limburger; Limburgish',
|
||||
u'LIN': u'Lingala',
|
||||
u'LIT': u'Lithuanian',
|
||||
u'EFI': u'Efik',
|
||||
u'BTK': u'Batak (Indonesia)',
|
||||
u'KAC': u'Kachin',
|
||||
u'KAB': u'Kabyle',
|
||||
u'KAA': u'Kara-Kalpak',
|
||||
u'KAN': u'Kannada',
|
||||
u'KAM': u'Kamba',
|
||||
u'KAL': u'Kalaallisut; Greenlandic',
|
||||
u'KAS': u'Kashmiri',
|
||||
u'KAR': u'Karen',
|
||||
u'KAU': u'Kanuri',
|
||||
u'KAT': u'Georgian',
|
||||
u'KAZ': u'Kazakh',
|
||||
u'TYV': u'Tuvinian',
|
||||
u'AWA': u'Awadhi',
|
||||
u'URD': u'Urdu',
|
||||
u'DOI': u'Dogri',
|
||||
u'TPI': u'Tok Pisin',
|
||||
u'MRI': u'Maori',
|
||||
u'ABK': u'Abkhazian',
|
||||
u'TKL': u'Tokelau',
|
||||
u'NLD': u'Dutch; Flemish',
|
||||
u'OJI': u'Ojibwa',
|
||||
u'OCI': u'Occitan (post 1500); Provençal',
|
||||
u'WOL': u'Wolof',
|
||||
u'JAV': u'Javanese',
|
||||
u'HRV': u'Croatian',
|
||||
u'DYU': u'Dyula',
|
||||
u'SSW': u'Swati',
|
||||
u'MUL': u'Multiple languages',
|
||||
u'HIL': u'Hiligaynon',
|
||||
u'HIM': u'Himachali',
|
||||
u'HIN': u'Hindi',
|
||||
u'BAS': u'Basa',
|
||||
u'GBA': u'Gbaya',
|
||||
u'WLN': u'Walloon',
|
||||
u'BAD': u'Banda',
|
||||
u'NEP': u'Nepali',
|
||||
u'CRE': u'Cree',
|
||||
u'BAN': u'Balinese',
|
||||
u'BAL': u'Baluchi',
|
||||
u'BAM': u'Bambara',
|
||||
u'BAK': u'Bashkir',
|
||||
u'SHN': u'Shan',
|
||||
u'ARP': u'Arapaho',
|
||||
u'ARW': u'Arawak',
|
||||
u'ARA': u'Arabic',
|
||||
u'ARC': u'Aramaic',
|
||||
u'ARG': u'Aragonese',
|
||||
u'SEL': u'Selkup',
|
||||
u'ARN': u'Araucanian',
|
||||
u'LUS': u'Lushai',
|
||||
u'MUS': u'Creek',
|
||||
u'LUA': u'Luba-Lulua',
|
||||
u'LUB': u'Luba-Katanga',
|
||||
u'LUG': u'Ganda',
|
||||
u'LUI': u'Luiseno',
|
||||
u'LUN': u'Lunda',
|
||||
u'LUO': u'Luo (Kenya and Tanzania)',
|
||||
u'IKU': u'Inuktitut',
|
||||
u'TUR': u'Turkish',
|
||||
u'TUK': u'Turkmen',
|
||||
u'TUM': u'Tumbuka',
|
||||
u'COP': u'Coptic',
|
||||
u'COS': u'Corsican',
|
||||
u'COR': u'Cornish',
|
||||
u'ILO': u'Iloko',
|
||||
u'GWI': u'Gwich´in',
|
||||
u'TLI': u'Tlingit',
|
||||
u'TLH': u'Klingon; tlhIngan-Hol',
|
||||
u'POR': u'Portuguese',
|
||||
u'PON': u'Pohnpeian',
|
||||
u'POL': u'Polish',
|
||||
u'TGK': u'Tajik',
|
||||
u'TGL': u'Tagalog',
|
||||
u'FRA': u'French',
|
||||
u'BHO': u'Bhojpuri',
|
||||
u'SWA': u'Swahili',
|
||||
u'DUA': u'Duala',
|
||||
u'SWE': u'Swedish',
|
||||
u'YAP': u'Yapese',
|
||||
u'TIV': u'Tiv',
|
||||
u'YAO': u'Yao',
|
||||
u'XAL': u'Kalmyk',
|
||||
u'FRY': u'Frisian',
|
||||
u'GAY': u'Gayo',
|
||||
u'OTA': u'Turkish, Ottoman (1500-1928)',
|
||||
u'HMN': u'Hmong',
|
||||
u'HMO': u'Hiri Motu',
|
||||
u'GAA': u'Ga',
|
||||
u'FUR': u'Friulian',
|
||||
u'MLG': u'Malagasy',
|
||||
u'SLV': u'Slovenian',
|
||||
u'FIL': u'Filipino; Pilipino',
|
||||
u'MLT': u'Maltese',
|
||||
u'SLK': u'Slovak',
|
||||
u'FUL': u'Fulah',
|
||||
u'JPN': u'Japanese',
|
||||
u'VOL': u'Volapük',
|
||||
u'VOT': u'Votic',
|
||||
u'IND': u'Indonesian',
|
||||
u'AVE': u'Avestan',
|
||||
u'JPR': u'Judeo-Persian',
|
||||
u'AVA': u'Avaric',
|
||||
u'PAP': u'Papiamento',
|
||||
u'EWO': u'Ewondo',
|
||||
u'PAU': u'Palauan',
|
||||
u'EWE': u'Ewe',
|
||||
u'PAG': u'Pangasinan',
|
||||
u'PAM': u'Pampanga',
|
||||
u'PAN': u'Panjabi; Punjabi',
|
||||
u'KIR': u'Kirghiz',
|
||||
u'NIA': u'Nias',
|
||||
u'KIK': u'Kikuyu; Gikuyu',
|
||||
u'SYR': u'Syriac',
|
||||
u'KIN': u'Kinyarwanda',
|
||||
u'NIU': u'Niuean',
|
||||
u'EPO': u'Esperanto',
|
||||
u'JBO': u'Lojban',
|
||||
u'MIC': u'Mi\'kmaq; Micmac',
|
||||
u'THA': u'Thai',
|
||||
u'HAI': u'Haida',
|
||||
u'ELL': u'Greek, Modern (1453-)',
|
||||
u'ADY': u'Adyghe; Adygei',
|
||||
u'ELX': u'Elamite',
|
||||
u'ADA': u'Adangme',
|
||||
u'GRB': u'Grebo',
|
||||
u'HAT': u'Haitian; Haitian Creole',
|
||||
u'HAU': u'Hausa',
|
||||
u'HAW': u'Hawaiian',
|
||||
u'BIN': u'Bini',
|
||||
u'AMH': u'Amharic',
|
||||
u'BIK': u'Bikol',
|
||||
u'BIH': u'Bihari',
|
||||
u'MOS': u'Mossi',
|
||||
u'MOH': u'Mohawk',
|
||||
u'MON': u'Mongolian',
|
||||
u'MOL': u'Moldavian',
|
||||
u'BIS': u'Bislama',
|
||||
u'TVL': u'Tuvalu',
|
||||
u'IJO': u'Ijo',
|
||||
u'EST': u'Estonian',
|
||||
u'KMB': u'Kimbundu',
|
||||
u'UMB': u'Umbundu',
|
||||
u'TMH': u'Tamashek',
|
||||
u'FON': u'Fon',
|
||||
u'HSB': u'Upper Sorbian',
|
||||
u'RUN': u'Rundi',
|
||||
u'RUS': u'Russian',
|
||||
u'PLI': u'Pali',
|
||||
u'SRD': u'Sardinian',
|
||||
u'ACH': u'Acoli',
|
||||
u'NDE': u'Ndebele, North; North Ndebele',
|
||||
u'DZO': u'Dzongkha',
|
||||
u'KRU': u'Kurukh',
|
||||
u'SRR': u'Serer',
|
||||
u'IDO': u'Ido',
|
||||
u'SRP': u'Serbian',
|
||||
u'KRO': u'Kru',
|
||||
u'KRC': u'Karachay-Balkar',
|
||||
u'NDS': u'Low German; Low Saxon; German, Low; Saxon, Low',
|
||||
u'ZUN': u'Zuni',
|
||||
u'ZUL': u'Zulu',
|
||||
u'TWI': u'Twi',
|
||||
u'NSO': u'Northern Sotho, Pedi; Sepedi',
|
||||
u'SOM': u'Somali',
|
||||
u'SON': u'Songhai',
|
||||
u'SOT': u'Sotho, Southern',
|
||||
u'MKD': u'Macedonian',
|
||||
u'HER': u'Herero',
|
||||
u'LOL': u'Mongo',
|
||||
u'HEB': u'Hebrew',
|
||||
u'LOZ': u'Lozi',
|
||||
u'GIL': u'Gilbertese',
|
||||
u'WAS': u'Washo',
|
||||
u'WAR': u'Waray',
|
||||
u'BUL': u'Bulgarian',
|
||||
u'WAL': u'Walamo',
|
||||
u'BUA': u'Buriat',
|
||||
u'BUG': u'Buginese',
|
||||
u'AZE': u'Azerbaijani',
|
||||
u'ZHA': u'Zhuang; Chuang',
|
||||
u'ZHO': u'Chinese',
|
||||
u'NNO': u'Norwegian Nynorsk; Nynorsk, Norwegian',
|
||||
u'UIG': u'Uighur; Uyghur',
|
||||
u'MYV': u'Erzya',
|
||||
u'INH': u'Ingush',
|
||||
u'KHM': u'Khmer',
|
||||
u'MYA': u'Burmese',
|
||||
u'KHA': u'Khasi',
|
||||
u'INA': u'Interlingua (International Auxiliary Language Association)',
|
||||
u'NAH': u'Nahuatl',
|
||||
u'TIR': u'Tigrinya',
|
||||
u'NAP': u'Neapolitan',
|
||||
u'NAV': u'Navajo; Navaho',
|
||||
u'NAU': u'Nauru',
|
||||
u'GRN': u'Guarani',
|
||||
u'TIG': u'Tigre',
|
||||
u'YOR': u'Yoruba',
|
||||
u'ILE': u'Interlingue',
|
||||
u'SQI': u'Albanian',
|
||||
}
|
||||
|
||||
# EOF
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__revision__ = '$Id: releasetypenames.py 8728 2006-12-17 23:42:30Z luks $'
|
||||
|
||||
releaseTypeNames = {
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#None': u'None',
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#Album': u'Album',
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#Single': u'Single',
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#EP': u'EP',
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#Compilation': u'Compilation',
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#Soundtrack': u'Soundtrack',
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#Spokenword': u'Spokenword',
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#Interview': u'Interview',
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#Audiobook': u'Audiobook',
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#Live': u'Live',
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#Remix': u'Remix',
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#Other': u'Other',
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#Official': u'Official',
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#Promotion': u'Promotion',
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#Bootleg': u'Bootleg',
|
||||
u'http://musicbrainz.org/ns/mmd-1.0#Pseudo-Release': u'Pseudo-Release',
|
||||
}
|
||||
|
||||
# EOF
|
||||
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__revision__ = '$Id: scriptnames.py 7386 2006-04-30 11:12:55Z matt $'
|
||||
|
||||
scriptNames = {
|
||||
u'Yiii': u'Yi',
|
||||
u'Telu': u'Telugu',
|
||||
u'Taml': u'Tamil',
|
||||
u'Guru': u'Gurmukhi',
|
||||
u'Hebr': u'Hebrew',
|
||||
u'Geor': u'Georgian (Mkhedruli)',
|
||||
u'Ugar': u'Ugaritic',
|
||||
u'Cyrl': u'Cyrillic',
|
||||
u'Hrkt': u'Kanji & Kana',
|
||||
u'Armn': u'Armenian',
|
||||
u'Runr': u'Runic',
|
||||
u'Khmr': u'Khmer',
|
||||
u'Latn': u'Latin',
|
||||
u'Hani': u'Han (Hanzi, Kanji, Hanja)',
|
||||
u'Ital': u'Old Italic (Etruscan, Oscan, etc.)',
|
||||
u'Hano': u'Hanunoo (Hanunóo)',
|
||||
u'Ethi': u'Ethiopic (Ge\'ez)',
|
||||
u'Gujr': u'Gujarati',
|
||||
u'Hang': u'Hangul',
|
||||
u'Arab': u'Arabic',
|
||||
u'Thaa': u'Thaana',
|
||||
u'Buhd': u'Buhid',
|
||||
u'Sinh': u'Sinhala',
|
||||
u'Orya': u'Oriya',
|
||||
u'Hans': u'Han (Simplified variant)',
|
||||
u'Thai': u'Thai',
|
||||
u'Cprt': u'Cypriot',
|
||||
u'Linb': u'Linear B',
|
||||
u'Hant': u'Han (Traditional variant)',
|
||||
u'Osma': u'Osmanya',
|
||||
u'Mong': u'Mongolian',
|
||||
u'Deva': u'Devanagari (Nagari)',
|
||||
u'Laoo': u'Lao',
|
||||
u'Tagb': u'Tagbanwa',
|
||||
u'Hira': u'Hiragana',
|
||||
u'Bopo': u'Bopomofo',
|
||||
u'Goth': u'Gothic',
|
||||
u'Tale': u'Tai Le',
|
||||
u'Mymr': u'Myanmar (Burmese)',
|
||||
u'Tglg': u'Tagalog',
|
||||
u'Grek': u'Greek',
|
||||
u'Mlym': u'Malayalam',
|
||||
u'Cher': u'Cherokee',
|
||||
u'Tibt': u'Tibetan',
|
||||
u'Kana': u'Katakana',
|
||||
u'Syrc': u'Syriac',
|
||||
u'Cans': u'Unified Canadian Aboriginal Syllabics',
|
||||
u'Beng': u'Bengali',
|
||||
u'Limb': u'Limbu',
|
||||
u'Ogam': u'Ogham',
|
||||
u'Knda': u'Kannada',
|
||||
}
|
||||
|
||||
# EOF
|
||||
@@ -0,0 +1,221 @@
|
||||
"""Utilities for working with Audio CDs.
|
||||
|
||||
This module contains utilities for working with Audio CDs.
|
||||
|
||||
The functions in this module need both a working ctypes package (already
|
||||
included in python-2.5) and an installed libdiscid. If you don't have
|
||||
libdiscid, it can't be loaded, or your platform isn't supported by either
|
||||
ctypes or this module, a C{NotImplementedError} is raised when using the
|
||||
L{readDisc()} function.
|
||||
|
||||
@author: Matthias Friedrich <matt@mafr.de>
|
||||
"""
|
||||
__revision__ = '$Id: disc.py 11987 2009-08-22 11:57:51Z matt $'
|
||||
|
||||
import sys
|
||||
import urllib
|
||||
import urlparse
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
from lib.musicbrainz2.model import Disc
|
||||
|
||||
__all__ = [ 'DiscError', 'readDisc', 'getSubmissionUrl' ]
|
||||
|
||||
|
||||
class DiscError(IOError):
|
||||
"""The Audio CD could not be read.
|
||||
|
||||
This may be simply because no disc was in the drive, the device name
|
||||
was wrong or the disc can't be read. Reading errors can occur in case
|
||||
of a damaged disc or a copy protection mechanism, for example.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def _openLibrary():
|
||||
"""Tries to open libdiscid.
|
||||
|
||||
@return: a C{ctypes.CDLL} object, representing the opened library
|
||||
|
||||
@raise NotImplementedError: if the library can't be opened
|
||||
"""
|
||||
# This only works for ctypes >= 0.9.9.3. Any libdiscid is found,
|
||||
# no matter how it's called on this platform.
|
||||
try:
|
||||
if hasattr(ctypes.cdll, 'find'):
|
||||
libDiscId = ctypes.cdll.find('discid')
|
||||
_setPrototypes(libDiscId)
|
||||
return libDiscId
|
||||
except OSError, e:
|
||||
raise NotImplementedError('Error opening library: ' + str(e))
|
||||
|
||||
# Try to find the library using ctypes.util
|
||||
libName = ctypes.util.find_library('discid')
|
||||
if libName != None:
|
||||
try:
|
||||
libDiscId = ctypes.cdll.LoadLibrary(libName)
|
||||
_setPrototypes(libDiscId)
|
||||
return libDiscId
|
||||
except OSError, e:
|
||||
raise NotImplementedError('Error opening library: ' +
|
||||
str(e))
|
||||
|
||||
# For compatibility with ctypes < 0.9.9.3 try to figure out the library
|
||||
# name without the help of ctypes. We use cdll.LoadLibrary() below,
|
||||
# which isn't available for ctypes == 0.9.9.3.
|
||||
#
|
||||
if sys.platform == 'linux2':
|
||||
libName = 'libdiscid.so.0'
|
||||
elif sys.platform == 'darwin':
|
||||
libName = 'libdiscid.0.dylib'
|
||||
elif sys.platform == 'win32':
|
||||
libName = 'discid.dll'
|
||||
else:
|
||||
# This should at least work for Un*x-style operating systems
|
||||
libName = 'libdiscid.so.0'
|
||||
|
||||
try:
|
||||
libDiscId = ctypes.cdll.LoadLibrary(libName)
|
||||
_setPrototypes(libDiscId)
|
||||
return libDiscId
|
||||
except OSError, e:
|
||||
raise NotImplementedError('Error opening library: ' + str(e))
|
||||
|
||||
assert False # not reached
|
||||
|
||||
|
||||
def _setPrototypes(libDiscId):
|
||||
ct = ctypes
|
||||
libDiscId.discid_new.argtypes = ( )
|
||||
libDiscId.discid_new.restype = ct.c_void_p
|
||||
|
||||
libDiscId.discid_free.argtypes = (ct.c_void_p, )
|
||||
|
||||
libDiscId.discid_read.argtypes = (ct.c_void_p, ct.c_char_p)
|
||||
|
||||
libDiscId.discid_get_error_msg.argtypes = (ct.c_void_p, )
|
||||
libDiscId.discid_get_error_msg.restype = ct.c_char_p
|
||||
|
||||
libDiscId.discid_get_id.argtypes = (ct.c_void_p, )
|
||||
libDiscId.discid_get_id.restype = ct.c_char_p
|
||||
|
||||
libDiscId.discid_get_first_track_num.argtypes = (ct.c_void_p, )
|
||||
libDiscId.discid_get_first_track_num.restype = ct.c_int
|
||||
|
||||
libDiscId.discid_get_last_track_num.argtypes = (ct.c_void_p, )
|
||||
libDiscId.discid_get_last_track_num.restype = ct.c_int
|
||||
|
||||
libDiscId.discid_get_sectors.argtypes = (ct.c_void_p, )
|
||||
libDiscId.discid_get_sectors.restype = ct.c_int
|
||||
|
||||
libDiscId.discid_get_track_offset.argtypes = (ct.c_void_p, ct.c_int)
|
||||
libDiscId.discid_get_track_offset.restype = ct.c_int
|
||||
|
||||
libDiscId.discid_get_track_length.argtypes = (ct.c_void_p, ct.c_int)
|
||||
libDiscId.discid_get_track_length.restype = ct.c_int
|
||||
|
||||
|
||||
def getSubmissionUrl(disc, host='mm.musicbrainz.org', port=80):
|
||||
"""Returns a URL for adding a disc to the MusicBrainz database.
|
||||
|
||||
A fully initialized L{musicbrainz2.model.Disc} object is needed, as
|
||||
returned by L{readDisc}. A disc object returned by the web service
|
||||
doesn't provide the necessary information.
|
||||
|
||||
Note that the created URL is intended for interactive use and points
|
||||
to the MusicBrainz disc submission wizard by default. This method
|
||||
just returns a URL, no network connection is needed. The disc drive
|
||||
isn't used.
|
||||
|
||||
@param disc: a fully initialized L{musicbrainz2.model.Disc} object
|
||||
@param host: a string containing a host name
|
||||
@param port: an integer containing a port number
|
||||
|
||||
@return: a string containing the submission URL
|
||||
|
||||
@see: L{readDisc}
|
||||
"""
|
||||
assert isinstance(disc, Disc), 'musicbrainz2.model.Disc expected'
|
||||
discid = disc.getId()
|
||||
first = disc.getFirstTrackNum()
|
||||
last = disc.getLastTrackNum()
|
||||
sectors = disc.getSectors()
|
||||
assert None not in (discid, first, last, sectors)
|
||||
|
||||
tracks = last - first + 1
|
||||
toc = "%d %d %d " % (first, last, sectors)
|
||||
toc = toc + ' '.join( map(lambda x: str(x[0]), disc.getTracks()) )
|
||||
|
||||
query = urllib.urlencode({ 'id': discid, 'toc': toc, 'tracks': tracks })
|
||||
|
||||
if port == 80:
|
||||
netloc = host
|
||||
else:
|
||||
netloc = host + ':' + str(port)
|
||||
|
||||
url = ('http', netloc, '/bare/cdlookup.html', '', query, '')
|
||||
|
||||
return urlparse.urlunparse(url)
|
||||
|
||||
|
||||
def readDisc(deviceName=None):
|
||||
"""Reads an Audio CD in the disc drive.
|
||||
|
||||
This reads a CD's table of contents (TOC) and calculates the MusicBrainz
|
||||
DiscID, which is a 28 character ASCII string. This DiscID can be used
|
||||
to retrieve a list of matching releases from the web service (see
|
||||
L{musicbrainz2.webservice.Query}).
|
||||
|
||||
Note that an Audio CD has to be in drive for this to work. The
|
||||
C{deviceName} argument may be used to set the device. The default
|
||||
depends on the operating system (on linux, it's C{'/dev/cdrom'}).
|
||||
No network connection is needed for this function.
|
||||
|
||||
If the device doesn't exist or there's no valid Audio CD in the drive,
|
||||
a L{DiscError} exception is raised.
|
||||
|
||||
@param deviceName: a string containing the CD drive's device name
|
||||
|
||||
@return: a L{musicbrainz2.model.Disc} object
|
||||
|
||||
@raise DiscError: if there was a problem reading the disc
|
||||
@raise NotImplementedError: if DiscID generation isn't supported
|
||||
"""
|
||||
libDiscId = _openLibrary()
|
||||
|
||||
handle = libDiscId.discid_new()
|
||||
assert handle != 0, "libdiscid: discid_new() returned NULL"
|
||||
|
||||
# Access the CD drive. This also works if deviceName is None because
|
||||
# ctypes passes a NULL pointer in this case.
|
||||
#
|
||||
res = libDiscId.discid_read(handle, deviceName)
|
||||
if res == 0:
|
||||
raise DiscError(libDiscId.discid_get_error_msg(handle))
|
||||
|
||||
|
||||
# Now extract the data from the result.
|
||||
#
|
||||
disc = Disc()
|
||||
|
||||
disc.setId( libDiscId.discid_get_id(handle) )
|
||||
|
||||
firstTrackNum = libDiscId.discid_get_first_track_num(handle)
|
||||
lastTrackNum = libDiscId.discid_get_last_track_num(handle)
|
||||
|
||||
disc.setSectors(libDiscId.discid_get_sectors(handle))
|
||||
|
||||
for i in range(firstTrackNum, lastTrackNum+1):
|
||||
trackOffset = libDiscId.discid_get_track_offset(handle, i)
|
||||
trackSectors = libDiscId.discid_get_track_length(handle, i)
|
||||
|
||||
disc.addTrack( (trackOffset, trackSectors) )
|
||||
|
||||
disc.setFirstTrackNum(firstTrackNum)
|
||||
disc.setLastTrackNum(lastTrackNum)
|
||||
|
||||
libDiscId.discid_free(handle)
|
||||
|
||||
return disc
|
||||
|
||||
# EOF
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,204 @@
|
||||
"""Various utilities to simplify common tasks.
|
||||
|
||||
This module contains helper functions to make common tasks easier.
|
||||
|
||||
@author: Matthias Friedrich <matt@mafr.de>
|
||||
"""
|
||||
__revision__ = '$Id: utils.py 11853 2009-07-21 09:26:50Z luks $'
|
||||
|
||||
import re
|
||||
import urlparse
|
||||
import os.path
|
||||
|
||||
__all__ = [
|
||||
'extractUuid', 'extractFragment', 'extractEntityType',
|
||||
'getReleaseTypeName', 'getCountryName', 'getLanguageName',
|
||||
'getScriptName',
|
||||
]
|
||||
|
||||
|
||||
# A pattern to split the path part of an absolute MB URI.
|
||||
PATH_PATTERN = '^/(artist|release|track|label|release-group)/([^/]*)$'
|
||||
|
||||
|
||||
def extractUuid(uriStr, resType=None):
|
||||
"""Extract the UUID part from a MusicBrainz identifier.
|
||||
|
||||
This function takes a MusicBrainz ID (an absolute URI) as the input
|
||||
and returns the UUID part of the URI, thus turning it into a relative
|
||||
URI. If C{uriStr} is None or a relative URI, then it is returned
|
||||
unchanged.
|
||||
|
||||
The C{resType} parameter can be used for error checking. Set it to
|
||||
'artist', 'release', or 'track' to make sure C{uriStr} is a
|
||||
syntactically valid MusicBrainz identifier of the given resource
|
||||
type. If it isn't, a C{ValueError} exception is raised.
|
||||
This error checking only works if C{uriStr} is an absolute URI, of
|
||||
course.
|
||||
|
||||
Example:
|
||||
|
||||
>>> from musicbrainz2.utils import extractUuid
|
||||
>>> extractUuid('http://musicbrainz.org/artist/c0b2500e-0cef-4130-869d-732b23ed9df5', 'artist')
|
||||
'c0b2500e-0cef-4130-869d-732b23ed9df5'
|
||||
>>>
|
||||
|
||||
@param uriStr: a string containing a MusicBrainz ID (an URI), or None
|
||||
@param resType: a string containing a resource type
|
||||
|
||||
@return: a string containing a relative URI, or None
|
||||
|
||||
@raise ValueError: the given URI is no valid MusicBrainz ID
|
||||
"""
|
||||
if uriStr is None:
|
||||
return None
|
||||
|
||||
(scheme, netloc, path) = urlparse.urlparse(uriStr)[:3]
|
||||
|
||||
if scheme == '':
|
||||
return uriStr # no URI, probably already the UUID
|
||||
|
||||
if scheme != 'http' or netloc != 'musicbrainz.org':
|
||||
raise ValueError('%s is no MB ID.' % uriStr)
|
||||
|
||||
m = re.match(PATH_PATTERN, path)
|
||||
|
||||
if m:
|
||||
if resType is None:
|
||||
return m.group(2)
|
||||
else:
|
||||
if m.group(1) == resType:
|
||||
return m.group(2)
|
||||
else:
|
||||
raise ValueError('expected "%s" Id' % resType)
|
||||
else:
|
||||
raise ValueError('%s is no valid MB ID.' % uriStr)
|
||||
|
||||
|
||||
def extractFragment(uriStr, uriPrefix=None):
|
||||
"""Extract the fragment part from a URI.
|
||||
|
||||
If C{uriStr} is None or no absolute URI, then it is returned unchanged.
|
||||
|
||||
The C{uriPrefix} parameter can be used for error checking. If C{uriStr}
|
||||
is an absolute URI, then the function checks if it starts with
|
||||
C{uriPrefix}. If it doesn't, a C{ValueError} exception is raised.
|
||||
|
||||
@param uriStr: a string containing an absolute URI
|
||||
@param uriPrefix: a string containing an URI prefix
|
||||
|
||||
@return: a string containing the fragment, or None
|
||||
|
||||
@raise ValueError: the given URI doesn't start with C{uriPrefix}
|
||||
"""
|
||||
if uriStr is None:
|
||||
return None
|
||||
|
||||
(scheme, netloc, path, params, query, frag) = urlparse.urlparse(uriStr)
|
||||
if scheme == '':
|
||||
return uriStr # this is no URI
|
||||
|
||||
if uriPrefix is None or uriStr.startswith(uriPrefix):
|
||||
return frag
|
||||
else:
|
||||
raise ValueError("prefix doesn't match URI %s" % uriStr)
|
||||
|
||||
|
||||
def extractEntityType(uriStr):
|
||||
"""Returns the entity type an entity URI is referring to.
|
||||
|
||||
@param uriStr: a string containing an absolute entity URI
|
||||
|
||||
@return: a string containing 'artist', 'release', 'track', or 'label'
|
||||
|
||||
@raise ValueError: if the given URI is no valid MusicBrainz ID
|
||||
"""
|
||||
if uriStr is None:
|
||||
raise ValueError('None is no valid entity URI')
|
||||
|
||||
(scheme, netloc, path) = urlparse.urlparse(uriStr)[:3]
|
||||
|
||||
if scheme == '':
|
||||
raise ValueError('%s is no absolute MB ID.' % uriStr)
|
||||
|
||||
if scheme != 'http' or netloc != 'musicbrainz.org':
|
||||
raise ValueError('%s is no MB ID.' % uriStr)
|
||||
|
||||
m = re.match(PATH_PATTERN, path)
|
||||
|
||||
if m:
|
||||
return m.group(1)
|
||||
else:
|
||||
raise ValueError('%s is no valid MB ID.' % uriStr)
|
||||
|
||||
|
||||
def getReleaseTypeName(releaseType):
|
||||
"""Returns the name of a release type URI.
|
||||
|
||||
@param releaseType: a string containing a release type URI
|
||||
|
||||
@return: a string containing a printable name for the release type
|
||||
|
||||
@see: L{musicbrainz2.model.Release}
|
||||
"""
|
||||
from musicbrainz2.data.releasetypenames import releaseTypeNames
|
||||
return releaseTypeNames.get(releaseType)
|
||||
|
||||
|
||||
def getCountryName(id_):
|
||||
"""Returns a country's name based on an ISO-3166 country code.
|
||||
|
||||
The country table this function is based on has been modified for
|
||||
MusicBrainz purposes by using the extension mechanism defined in
|
||||
ISO-3166. All IDs are still valid ISO-3166 country codes, but some
|
||||
IDs have been added to include historic countries and some of the
|
||||
country names have been modified to make them better suited for
|
||||
display purposes.
|
||||
|
||||
If the country ID is not found, None is returned. This may happen
|
||||
for example, when new countries are added to the MusicBrainz web
|
||||
service which aren't known to this library yet.
|
||||
|
||||
@param id_: a two-letter upper case string containing an ISO-3166 code
|
||||
|
||||
@return: a string containing the country's name, or None
|
||||
|
||||
@see: L{musicbrainz2.model}
|
||||
"""
|
||||
from musicbrainz2.data.countrynames import countryNames
|
||||
return countryNames.get(id_)
|
||||
|
||||
|
||||
def getLanguageName(id_):
|
||||
"""Returns a language name based on an ISO-639-2/T code.
|
||||
|
||||
This function uses a subset of the ISO-639-2/T code table to map
|
||||
language IDs (terminologic, not bibliographic ones!) to names.
|
||||
|
||||
@param id_: a three-letter upper case string containing an ISO-639-2/T code
|
||||
|
||||
@return: a string containing the language's name, or None
|
||||
|
||||
@see: L{musicbrainz2.model}
|
||||
"""
|
||||
from musicbrainz2.data.languagenames import languageNames
|
||||
return languageNames.get(id_)
|
||||
|
||||
|
||||
def getScriptName(id_):
|
||||
"""Returns a script name based on an ISO-15924 code.
|
||||
|
||||
This function uses a subset of the ISO-15924 code table to map
|
||||
script IDs to names.
|
||||
|
||||
@param id_: a four-letter string containing an ISO-15924 script code
|
||||
|
||||
@return: a string containing the script's name, or None
|
||||
|
||||
@see: L{musicbrainz2.model}
|
||||
"""
|
||||
from musicbrainz2.data.scriptnames import scriptNames
|
||||
return scriptNames.get(id_)
|
||||
|
||||
|
||||
# EOF
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
from lib.pyItunes.Song import Song
|
||||
import time
|
||||
class Library:
|
||||
def __init__(self,dictionary):
|
||||
self.songs = self.parseDictionary(dictionary)
|
||||
|
||||
def parseDictionary(self,dictionary):
|
||||
songs = []
|
||||
format = "%Y-%m-%dT%H:%M:%SZ"
|
||||
for song,attributes in dictionary.iteritems():
|
||||
s = Song()
|
||||
s.name = attributes.get('Name')
|
||||
s.artist = attributes.get('Artist')
|
||||
s.album_artist = attributes.get('Album Aritst')
|
||||
s.composer = attributes.get('Composer')
|
||||
s.album = attributes.get('Album')
|
||||
s.genre = attributes.get('Genre')
|
||||
s.kind = attributes.get('Kind')
|
||||
if attributes.get('Size'):
|
||||
s.size = int(attributes.get('Size'))
|
||||
s.total_time = attributes.get('Total Time')
|
||||
s.track_number = attributes.get('Track Number')
|
||||
if attributes.get('Year'):
|
||||
s.year = int(attributes.get('Year'))
|
||||
if attributes.get('Date Modified'):
|
||||
s.date_modified = time.strptime(attributes.get('Date Modified'),format)
|
||||
if attributes.get('Date Added'):
|
||||
s.date_added = time.strptime(attributes.get('Date Added'),format)
|
||||
if attributes.get('Bit Rate'):
|
||||
s.bit_rate = int(attributes.get('Bit Rate'))
|
||||
if attributes.get('Sample Rate'):
|
||||
s.sample_rate = int(attributes.get('Sample Rate'))
|
||||
s.comments = attributes.get("Comments ")
|
||||
if attributes.get('Rating'):
|
||||
s.rating = int(attributes.get('Rating'))
|
||||
if attributes.get('Play Count'):
|
||||
s.play_count = int(attributes.get('Play Count'))
|
||||
if attributes.get('Location'):
|
||||
s.location = attributes.get('Location')
|
||||
songs.append(s)
|
||||
return songs
|
||||
@@ -0,0 +1,46 @@
|
||||
class Song:
|
||||
"""
|
||||
Song Attributes:
|
||||
name (String)
|
||||
artist (String)
|
||||
album_arist (String)
|
||||
composer = None (String)
|
||||
album = None (String)
|
||||
genre = None (String)
|
||||
kind = None (String)
|
||||
size = None (Integer)
|
||||
total_time = None (Integer)
|
||||
track_number = None (Integer)
|
||||
year = None (Integer)
|
||||
date_modified = None (Time)
|
||||
date_added = None (Time)
|
||||
bit_rate = None (Integer)
|
||||
sample_rate = None (Integer)
|
||||
comments = None (String)
|
||||
rating = None (Integer)
|
||||
album_rating = None (Integer)
|
||||
play_count = None (Integer)
|
||||
location = None (String)
|
||||
"""
|
||||
name = None
|
||||
artist = None
|
||||
album_arist = None
|
||||
composer = None
|
||||
album = None
|
||||
genre = None
|
||||
kind = None
|
||||
size = None
|
||||
total_time = None
|
||||
track_number = None
|
||||
year = None
|
||||
date_modified = None
|
||||
date_added = None
|
||||
bit_rate = None
|
||||
sample_rate = None
|
||||
comments = None
|
||||
rating = None
|
||||
album_rating = None
|
||||
play_count = None
|
||||
location = None
|
||||
|
||||
#title = property(getTitle,setTitle)
|
||||
@@ -0,0 +1,42 @@
|
||||
import re
|
||||
class XMLLibraryParser:
|
||||
def __init__(self,xmlLibrary):
|
||||
f = open(xmlLibrary)
|
||||
s = f.read()
|
||||
lines = s.split("\n")
|
||||
self.dictionary = self.parser(lines)
|
||||
|
||||
def getValue(self,restOfLine):
|
||||
value = re.sub("<.*?>","",restOfLine)
|
||||
u = unicode(value,"utf-8")
|
||||
cleanValue = u.encode("ascii","xmlcharrefreplace")
|
||||
return cleanValue
|
||||
|
||||
def keyAndRestOfLine(self,line):
|
||||
rawkey = re.search('<key>(.*?)</key>',line).group(0)
|
||||
key = re.sub("</*key>","",rawkey)
|
||||
restOfLine = re.sub("<key>.*?</key>","",line).strip()
|
||||
return key,restOfLine
|
||||
|
||||
def parser(self,lines):
|
||||
dicts = 0
|
||||
songs = {}
|
||||
inSong = False
|
||||
for line in lines:
|
||||
if re.search('<dict>',line):
|
||||
dicts += 1
|
||||
if re.search('</dict>',line):
|
||||
dicts -= 1
|
||||
inSong = False
|
||||
songs[songkey] = temp
|
||||
if dicts == 2 and re.search('<key>(.*?)</key>',line):
|
||||
rawkey = re.search('<key>(.*?)</key>',line).group(0)
|
||||
songkey = re.sub("</*key>","",rawkey)
|
||||
inSong = True
|
||||
temp = {}
|
||||
if dicts == 3 and re.search('<key>(.*?)</key>',line):
|
||||
key,restOfLine = self.keyAndRestOfLine(line)
|
||||
temp[key] = self.getValue(restOfLine)
|
||||
if len(songs) > 0 and dicts < 2:
|
||||
return songs
|
||||
return songs
|
||||
@@ -0,0 +1,3 @@
|
||||
from lib.pyItunes.XMLLibraryParser import XMLLibraryParser
|
||||
from lib.pyItunes.Library import Library
|
||||
from lib.pyItunes.Song import Song
|
||||
@@ -0,0 +1,25 @@
|
||||
# Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
# <http://www.opensource.org/licenses/mit-license.php>
|
||||
"""
|
||||
github module.
|
||||
"""
|
||||
__all__ = ['github','ghsearch','githubsync']
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
# <http://www.opensource.org/licenses/mit-license.php>
|
||||
"""
|
||||
Search script.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import github
|
||||
|
||||
def usage():
|
||||
"""display the usage and exit"""
|
||||
print "Usage: %s keyword [keyword...]" % (sys.argv[0])
|
||||
sys.exit(1)
|
||||
|
||||
def mk_url(repo):
|
||||
return "http://github.com/%s/%s" % (repo.username, repo.name)
|
||||
|
||||
if __name__ == '__main__':
|
||||
g = github.GitHub()
|
||||
if len(sys.argv) < 2:
|
||||
usage()
|
||||
res = g.repos.search(' '.join(sys.argv[1:]))
|
||||
|
||||
for repo in res:
|
||||
try:
|
||||
print "Found %s at %s" % (repo.name, mk_url(repo))
|
||||
except AttributeError:
|
||||
print "Bug: Couldn't format %s" % repo.__dict__
|
||||
@@ -0,0 +1,520 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
# <http://www.opensource.org/licenses/mit-license.php>
|
||||
"""
|
||||
Interface to github's API (v2).
|
||||
|
||||
Basic usage:
|
||||
|
||||
g = GitHub()
|
||||
|
||||
for r in g.user.search('dustin'):
|
||||
print r.name
|
||||
|
||||
See the GitHub docs or README.markdown for more usage.
|
||||
|
||||
Copyright (c) 2007 Dustin Sallings <dustin@spy.net>
|
||||
"""
|
||||
|
||||
# GAE friendly URL detection (theoretically)
|
||||
try:
|
||||
import urllib2
|
||||
default_fetcher = urllib2.urlopen
|
||||
except LoadError:
|
||||
pass
|
||||
|
||||
import urllib
|
||||
import xml
|
||||
import xml.dom.minidom
|
||||
|
||||
def _string_parser(x):
|
||||
"""Extract the data from the first child of the input."""
|
||||
return x.firstChild.data
|
||||
|
||||
_types = {
|
||||
'string': _string_parser,
|
||||
'integer': lambda x: int(_string_parser(x)),
|
||||
'float': lambda x: float(_string_parser(x)),
|
||||
'datetime': _string_parser,
|
||||
'boolean': lambda x: _string_parser(x) == 'true'
|
||||
}
|
||||
|
||||
def _parse(el):
|
||||
"""Generic response parser."""
|
||||
|
||||
type = 'string'
|
||||
if el.attributes and 'type' in el.attributes.keys():
|
||||
type = el.attributes['type'].value
|
||||
elif el.localName in _types:
|
||||
type = el.localName
|
||||
elif len(el.childNodes) > 1:
|
||||
# This is a container, find the child type
|
||||
type = None
|
||||
ch = el.firstChild
|
||||
while ch and not type:
|
||||
if ch.localName == 'type':
|
||||
type = ch.firstChild.data
|
||||
ch = ch.nextSibling
|
||||
|
||||
if not type:
|
||||
raise Exception("Can't parse %s, known: %s"
|
||||
% (el.toxml(), repr(_types.keys())))
|
||||
|
||||
return _types[type](el)
|
||||
|
||||
def parses(t):
|
||||
"""Parser for a specific type in the github response."""
|
||||
def f(orig):
|
||||
orig.parses = t
|
||||
return orig
|
||||
return f
|
||||
|
||||
def with_temporary_mappings(m):
|
||||
"""Allow temporary localized altering of type mappings."""
|
||||
def f(orig):
|
||||
def every(self, *args):
|
||||
global _types
|
||||
o = _types.copy()
|
||||
for k,v in m.items():
|
||||
if v:
|
||||
_types[k] = v
|
||||
else:
|
||||
del _types[k]
|
||||
try:
|
||||
return orig(self, *args)
|
||||
finally:
|
||||
_types = o
|
||||
return every
|
||||
return f
|
||||
|
||||
@parses('array')
|
||||
def _parseArray(el):
|
||||
rv = []
|
||||
ch = el.firstChild
|
||||
while ch:
|
||||
if ch.nodeType != xml.dom.Node.TEXT_NODE and ch.firstChild:
|
||||
rv.append(_parse(ch))
|
||||
ch=ch.nextSibling
|
||||
return rv
|
||||
|
||||
class BaseResponse(object):
|
||||
"""Base class for XML Response Handling."""
|
||||
|
||||
def __init__(self, el):
|
||||
ch = el.firstChild
|
||||
while ch:
|
||||
if ch.nodeType != xml.dom.Node.TEXT_NODE and ch.firstChild:
|
||||
ln = ch.localName.replace('-', '_')
|
||||
self.__dict__[ln] = _parse(ch)
|
||||
ch=ch.nextSibling
|
||||
|
||||
def __repr__(self):
|
||||
return "<<%s>>" % str(self.__class__)
|
||||
|
||||
class User(BaseResponse):
|
||||
"""A github user."""
|
||||
|
||||
parses = 'user'
|
||||
|
||||
def __repr__(self):
|
||||
return "<<User %s>>" % self.name
|
||||
|
||||
class Plan(BaseResponse):
|
||||
"""A github plan."""
|
||||
|
||||
parses = 'plan'
|
||||
|
||||
def __repr__(self):
|
||||
return "<<Plan %s>>" % self.name
|
||||
|
||||
class Repository(BaseResponse):
|
||||
"""A repository."""
|
||||
|
||||
parses = 'repository'
|
||||
|
||||
@property
|
||||
def owner_name(self):
|
||||
if hasattr(self, 'owner'):
|
||||
return self.owner
|
||||
else:
|
||||
return self.username
|
||||
|
||||
def __repr__(self):
|
||||
return "<<Repository %s/%s>>" % (self.owner_name, self.name)
|
||||
|
||||
class PublicKey(BaseResponse):
|
||||
"""A public key."""
|
||||
|
||||
parses = 'public-key'
|
||||
title = 'untitled'
|
||||
|
||||
def __repr__(self):
|
||||
return "<<Public key %s>>" % self.title
|
||||
|
||||
class Commit(BaseResponse):
|
||||
"""A commit."""
|
||||
|
||||
parses = 'commit'
|
||||
|
||||
def __repr__(self):
|
||||
return "<<Commit: %s>>" % self.id
|
||||
|
||||
class Parent(Commit):
|
||||
"""A commit parent."""
|
||||
|
||||
parses = 'parent'
|
||||
|
||||
class Author(User):
|
||||
"""A commit author."""
|
||||
|
||||
parses = 'author'
|
||||
|
||||
class Committer(User):
|
||||
"""A commit committer."""
|
||||
|
||||
parses = 'committer'
|
||||
|
||||
class Issue(BaseResponse):
|
||||
"""An issue within the issue tracker."""
|
||||
|
||||
parses = 'issue'
|
||||
|
||||
def __repr__(self):
|
||||
return "<<Issue #%d>>" % self.number
|
||||
|
||||
class Label(BaseResponse):
|
||||
"""A Label within the issue tracker."""
|
||||
parses = 'label'
|
||||
|
||||
def __repr__(self):
|
||||
return "<<Label $%d>>" % self.number
|
||||
|
||||
class Tree(BaseResponse):
|
||||
"""A Tree object."""
|
||||
|
||||
# Parsing is scoped to objects...
|
||||
def __repr__(self):
|
||||
return "<<Tree: %s>>" % self.name
|
||||
|
||||
class Blob(BaseResponse):
|
||||
"""A Blob object."""
|
||||
|
||||
# Parsing is scoped to objects...
|
||||
def __repr__(self):
|
||||
return "<<Blob: %s>>" % self.name
|
||||
|
||||
class Modification(BaseResponse):
|
||||
"""A modification object."""
|
||||
|
||||
# Parsing is scoped to usage
|
||||
def __repr__(self):
|
||||
return "<<Modification of %s>>" % self.filename
|
||||
|
||||
class Network(BaseResponse):
|
||||
"""A network entry."""
|
||||
|
||||
parses = 'network'
|
||||
|
||||
def __repr__(self):
|
||||
return "<<Network of %s/%s>>" % (self.owner, self.name)
|
||||
|
||||
# Load the known types.
|
||||
for __t in (t for t in globals().values() if hasattr(t, 'parses')):
|
||||
_types[__t.parses] = __t
|
||||
|
||||
class BaseEndpoint(object):
|
||||
|
||||
BASE_URL = 'http://github.com/api/v2/xml/'
|
||||
|
||||
def __init__(self, user, token, fetcher):
|
||||
self.user = user
|
||||
self.token = token
|
||||
self.fetcher = fetcher
|
||||
|
||||
def _raw_fetch(self, path):
|
||||
p = self.BASE_URL + path
|
||||
args = ''
|
||||
if self.user and self.token:
|
||||
params = '&'.join(['login=' + urllib.quote(self.user),
|
||||
'token=' + urllib.quote(self.token)])
|
||||
if '?' in path:
|
||||
p += params
|
||||
else:
|
||||
p += '?' + params
|
||||
return self.fetcher(p).read()
|
||||
|
||||
def _fetch(self, path):
|
||||
return xml.dom.minidom.parseString(self._raw_fetch(path))
|
||||
|
||||
def _post(self, path, **kwargs):
|
||||
p = {'login': self.user, 'token': self.token}
|
||||
p.update(kwargs)
|
||||
return self.fetcher(self.BASE_URL + path, urllib.urlencode(p)).read()
|
||||
|
||||
def _parsed(self, path):
|
||||
doc = self._fetch(path)
|
||||
return _parse(doc.documentElement)
|
||||
|
||||
def _posted(self,path,**kwargs):
|
||||
stuff = self._post(path,**kwargs)
|
||||
doc = xml.dom.minidom.parseString(stuff)
|
||||
return _parse(doc.documentElement)
|
||||
|
||||
class UserEndpoint(BaseEndpoint):
|
||||
|
||||
def search(self, query):
|
||||
"""Search for a user."""
|
||||
return self._parsed('user/search/' + query)
|
||||
|
||||
def show(self, username):
|
||||
"""Get the info for a user."""
|
||||
return self._parsed('user/show/' + username)
|
||||
|
||||
def keys(self):
|
||||
"""Get the public keys for a user."""
|
||||
return self._parsed('user/keys')
|
||||
|
||||
def removeKey(self, keyId):
|
||||
"""Remove the key with the given ID (as retrieved from keys)"""
|
||||
self._post('user/key/remove', id=keyId)
|
||||
|
||||
def addKey(self, name, key):
|
||||
"""Add an ssh key."""
|
||||
self._post('user/key/add', name=name, key=key)
|
||||
|
||||
class RepositoryEndpoint(BaseEndpoint):
|
||||
|
||||
def forUser(self, username):
|
||||
"""Get the repositories for the given user."""
|
||||
return self._parsed('repos/show/' + username)
|
||||
|
||||
def branches(self, user, repo):
|
||||
"""List the branches for a repo."""
|
||||
doc = self._fetch("repos/show/" + user + "/" + repo + "/branches")
|
||||
rv = {}
|
||||
for c in doc.documentElement.childNodes:
|
||||
if c.nodeType != xml.dom.Node.TEXT_NODE:
|
||||
rv[c.localName] = str(c.firstChild.data)
|
||||
return rv
|
||||
|
||||
def search(self, term):
|
||||
"""Search for repositories."""
|
||||
return self._parsed('repos/search/' + urllib.quote_plus(term))
|
||||
|
||||
def show(self, user, repo):
|
||||
"""Lookup an individual repository."""
|
||||
return self._parsed('/'.join(['repos', 'show', user, repo]))
|
||||
|
||||
def watch(self, user, repo):
|
||||
"""Watch a repository."""
|
||||
self._post('repos/watch/' + user + '/' + repo)
|
||||
|
||||
def unwatch(self, user, repo):
|
||||
"""Stop watching a repository."""
|
||||
self._post('repos/unwatch/' + user + '/' + repo)
|
||||
|
||||
def watched(self, user):
|
||||
"""Get watched repositories of a user."""
|
||||
return self._parsed('repos/watched/' + user)
|
||||
|
||||
def network(self, user, repo):
|
||||
"""Get the network for a given repo."""
|
||||
return self._parsed('repos/show/' + user + '/' + repo + '/network')
|
||||
|
||||
def setVisible(self, repo, public=True):
|
||||
"""Set the visibility of the given repository (owned by the current user)."""
|
||||
if public:
|
||||
path = 'repos/set/public/' + repo
|
||||
else:
|
||||
path = 'repos/set/private/' + repo
|
||||
self._post(path)
|
||||
|
||||
def create(self, name, description='', homepage='', public=1):
|
||||
"""Create a new repository."""
|
||||
self._post('repos/create', name=name, description=description,
|
||||
homepage=homepage, public=str(public))
|
||||
|
||||
def delete(self, repo):
|
||||
"""Delete a repository."""
|
||||
self._post('repos/delete/' + repo)
|
||||
|
||||
def fork(self, user, repo):
|
||||
"""Fork a user's repo."""
|
||||
self._post('repos/fork/' + user + '/' + repo)
|
||||
|
||||
def collaborators(self, user, repo):
|
||||
"""Find all of the collaborators of one of your repositories."""
|
||||
return self._parsed('repos/show/%s/%s/collaborators' % (user, repo))
|
||||
|
||||
def addCollaborator(self, repo, username):
|
||||
"""Add a collaborator to one of your repositories."""
|
||||
self._post('repos/collaborators/' + repo + '/add/' + username)
|
||||
|
||||
def removeCollaborator(self, repo, username):
|
||||
"""Remove a collaborator from one of your repositories."""
|
||||
self._post('repos/collaborators/' + repo + '/remove/' + username)
|
||||
|
||||
def collaborators_all(self):
|
||||
"""Find all of the collaborators of every of your repositories.
|
||||
|
||||
Returns a dictionary with reponame as key and a list of collaborators as value."""
|
||||
ret = {}
|
||||
for reponame in (rp.name for rp in self.forUser(self.user)):
|
||||
ret[reponame] = self.collaborators(self.user, reponame)
|
||||
return ret
|
||||
|
||||
def addCollaborator_all(self, username):
|
||||
"""Add a collaborator to all of your repositories."""
|
||||
for reponame in (rp.name for rp in self.forUser(self.user)):
|
||||
self.addCollaborator(reponame, username)
|
||||
|
||||
def removeCollaborator_all(self, username):
|
||||
"""Remove a collaborator from all of your repositories."""
|
||||
for reponame in (rp.name for rp in self.forUser(self.user)):
|
||||
self.removeCollaborator(reponame, username)
|
||||
|
||||
def deployKeys(self, repo):
|
||||
"""List the deploy keys for the given repository.
|
||||
|
||||
The repository must be owned by the current user."""
|
||||
return self._parsed('repos/keys/' + repo)
|
||||
|
||||
def addDeployKey(self, repo, title, key):
|
||||
"""Add a deploy key to a repository."""
|
||||
self._post('repos/key/' + repo + '/add', title=title, key=key)
|
||||
|
||||
def removeDeployKey(self, repo, keyId):
|
||||
"""Remove a deploy key."""
|
||||
self._post('repos/key/' + repo + '/remove', id=keyId)
|
||||
|
||||
class CommitEndpoint(BaseEndpoint):
|
||||
|
||||
def forBranch(self, user, repo, branch='master'):
|
||||
"""Get the commits for the given branch."""
|
||||
return self._parsed('/'.join(['commits', 'list', user, repo, branch]))
|
||||
|
||||
def forFile(self, user, repo, path, branch='master'):
|
||||
"""Get the commits for the given file within the given branch."""
|
||||
return self._parsed('/'.join(['commits', 'list', user, repo, branch, path]))
|
||||
|
||||
@with_temporary_mappings({'removed': _parseArray,
|
||||
'added': _parseArray,
|
||||
'modified': Modification,
|
||||
'diff': _string_parser,
|
||||
'filename': _string_parser})
|
||||
def show(self, user, repo, sha):
|
||||
"""Get an individual commit."""
|
||||
c = self._parsed('/'.join(['commits', 'show', user, repo, sha]))
|
||||
# Some fixup due to weird XML structure
|
||||
if hasattr(c, 'removed'):
|
||||
c.removed = [i[0] for i in c.removed]
|
||||
if hasattr(c, 'added'):
|
||||
c.added = [i[0] for i in c.added]
|
||||
return c
|
||||
|
||||
class IssuesEndpoint(BaseEndpoint):
|
||||
|
||||
@with_temporary_mappings({'user': None})
|
||||
def list(self, user, repo, state='open'):
|
||||
"""Get the list of issues for the given repo in the given state."""
|
||||
return self._parsed('/'.join(['issues', 'list', user, repo, state]))
|
||||
|
||||
@with_temporary_mappings({'user': None})
|
||||
def show(self, user, repo, issue_id):
|
||||
"""Show an individual issue."""
|
||||
return self._parsed('/'.join(['issues', 'show', user, repo, str(issue_id)]))
|
||||
|
||||
def add_label(self, user, repo, issue_id, label):
|
||||
"""Add a label to an issue."""
|
||||
self._post('issues/label/add/' + user + '/'
|
||||
+ repo + '/' + label + '/' + str(issue_id))
|
||||
|
||||
def remove_label(self, user, repo, issue_id, label):
|
||||
"""Remove a label from an issue."""
|
||||
self._post('issues/label/remove/' + user + '/'
|
||||
+ repo + '/' + label + '/' + str(issue_id))
|
||||
|
||||
def close(self, user, repo, issue_id):
|
||||
"""Close an issue."""
|
||||
self._post('/'.join(['issues', 'close', user, repo, str(issue_id)]))
|
||||
|
||||
def reopen(self, user, repo, issue_id):
|
||||
"""Reopen an issue."""
|
||||
self._post('/'.join(['issues', 'reopen', user, repo, str(issue_id)]))
|
||||
|
||||
def new(self, user, repo, title, body=''):
|
||||
"""Create a new issue."""
|
||||
return self._posted('/'.join(['issues', 'open', user, repo]),
|
||||
title=title, body=body)
|
||||
|
||||
def edit(self, user, repo, issue_id, title, body):
|
||||
"""Create a new issue."""
|
||||
self._post('/'.join(['issues', 'edit', user, repo, str(issue_id)]),
|
||||
title=title, body=body)
|
||||
|
||||
class ObjectsEndpoint(BaseEndpoint):
|
||||
|
||||
@with_temporary_mappings({'tree': Tree, 'type': _string_parser})
|
||||
def tree(self, user, repo, t):
|
||||
"""Get the given tree from the given repo."""
|
||||
tl = self._parsed('/'.join(['tree', 'show', user, repo, t]))
|
||||
return dict([(t.name, t) for t in tl])
|
||||
|
||||
@with_temporary_mappings({'blob': Blob})
|
||||
def blob(self, user, repo, t, fn):
|
||||
return self._parsed('/'.join(['blob', 'show', user, repo, t, fn]))
|
||||
|
||||
def raw_blob(self, user, repo, sha):
|
||||
"""Get a raw blob from a repo."""
|
||||
path = 'blob/show/%s/%s/%s' % (user, repo, sha)
|
||||
return self._raw_fetch(path)
|
||||
|
||||
class GitHub(object):
|
||||
"""Interface to github."""
|
||||
|
||||
def __init__(self, user=None, token=None, fetcher=default_fetcher):
|
||||
self.user = user
|
||||
self.token = token
|
||||
self.fetcher = fetcher
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
"""Get access to the user API."""
|
||||
return UserEndpoint(self.user, self.token, self.fetcher)
|
||||
|
||||
@property
|
||||
def repos(self):
|
||||
"""Get access to the user API."""
|
||||
return RepositoryEndpoint(self.user, self.token, self.fetcher)
|
||||
|
||||
@property
|
||||
def commits(self):
|
||||
return CommitEndpoint(self.user, self.token, self.fetcher)
|
||||
|
||||
@property
|
||||
def issues(self):
|
||||
return IssuesEndpoint(self.user, self.token, self.fetcher)
|
||||
|
||||
@property
|
||||
def objects(self):
|
||||
return ObjectsEndpoint(self.user, self.token, self.fetcher)
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
# <http://www.opensource.org/licenses/mit-license.php>
|
||||
"""
|
||||
Grab all of a user's projects from github.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
import github
|
||||
|
||||
def check_for_old_format(path, url):
|
||||
p = subprocess.Popen(['git', '--git-dir=' + path, 'config',
|
||||
'remote.origin.fetch'], stdout = subprocess.PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
if stdout.strip() != '+refs/*:refs/*':
|
||||
print "Not properly configured for mirroring, repairing."
|
||||
subprocess.call(['git', '--git-dir=' + path, 'remote', 'rm', 'origin'])
|
||||
add_mirror(path, url)
|
||||
|
||||
def add_mirror(path, url):
|
||||
subprocess.call(['git', '--git-dir=' + path, 'remote', 'add', '--mirror',
|
||||
'origin', url])
|
||||
|
||||
def sync(path, url, repo_name):
|
||||
p = os.path.join(path, repo_name) + ".git"
|
||||
print "Syncing %s -> %s" % (repo_name, p)
|
||||
if not os.path.exists(p):
|
||||
subprocess.call(['git', 'clone', '--bare', url, p])
|
||||
add_mirror(p, url)
|
||||
check_for_old_format(p, url)
|
||||
subprocess.call(['git', '--git-dir=' + p, 'fetch', '-f'])
|
||||
|
||||
def sync_user_repo(path, repo):
|
||||
sync(path, "git://github.com/%s/%s" % (repo.owner, repo.name), repo.name)
|
||||
|
||||
def usage():
|
||||
sys.stderr.write("Usage: %s username destination_url\n" % sys.argv[0])
|
||||
sys.stderr.write(
|
||||
"""Ensures you've got the latest stuff for the given user.
|
||||
|
||||
Also, if the file $HOME/.github-private exists, it will be read for
|
||||
additional projects.
|
||||
|
||||
Each line must be a simple project name (e.g. py-github), a tab character,
|
||||
and a git URL.
|
||||
""")
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
user, path = sys.argv[1:]
|
||||
except ValueError:
|
||||
usage()
|
||||
exit(1)
|
||||
|
||||
privfile = os.path.join(os.getenv("HOME"), ".github-private")
|
||||
if os.path.exists(privfile):
|
||||
f = open(privfile)
|
||||
for line in f:
|
||||
name, url = line.strip().split("\t")
|
||||
sync(path, url, name)
|
||||
|
||||
gh = github.GitHub()
|
||||
|
||||
for repo in gh.repos.forUser(user):
|
||||
sync_user_repo(path, repo)
|
||||
@@ -0,0 +1,493 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
# <http://www.opensource.org/licenses/mit-license.php>
|
||||
"""
|
||||
Defines and runs unittests.
|
||||
"""
|
||||
|
||||
import urllib
|
||||
import hashlib
|
||||
import unittest
|
||||
|
||||
import github
|
||||
|
||||
class BaseCase(unittest.TestCase):
|
||||
|
||||
def _gh(self, expUrl, filename):
|
||||
|
||||
def opener(url):
|
||||
self.assertEquals(expUrl, url)
|
||||
return open(filename)
|
||||
return github.GitHub(fetcher=opener)
|
||||
|
||||
def _agh(self, expUrl, u, t, filename):
|
||||
|
||||
def opener(url):
|
||||
self.assertEquals(expUrl, url + '?login=' + u + '&token=' + t)
|
||||
return open(filename)
|
||||
return github.GitHub(fetcher=opener)
|
||||
|
||||
def _ghp(self, expUrl, u, t, **kv):
|
||||
|
||||
def opener(url, data):
|
||||
h = {'login': u, 'token': t}
|
||||
h.update(kv)
|
||||
self.assertEquals(github.BaseEndpoint.BASE_URL + expUrl, url)
|
||||
self.assertEquals(sorted(data.split('&')),
|
||||
sorted(urllib.urlencode(h).split('&')))
|
||||
return github.GitHub(u, t, fetcher=opener)
|
||||
|
||||
class UserTest(BaseCase):
|
||||
|
||||
def __loadUserSearch(self):
|
||||
return self._gh('http://github.com/api/v2/xml/user/search/dustin',
|
||||
'data/user.search.xml').users.search('dustin')
|
||||
|
||||
def __loadUser(self, which, u=None, p=None):
|
||||
if u:
|
||||
return self._agh('http://github.com/api/v2/xml/user/show/dustin'
|
||||
+ '?login=' + u + '&token=' + p,
|
||||
u, p, 'data/' + which).users.show('dustin')
|
||||
|
||||
else:
|
||||
return self._gh('http://github.com/api/v2/xml/user/show/dustin',
|
||||
'data/' + which).users.show('dustin')
|
||||
|
||||
def testUserSearch(self):
|
||||
"""Test the base properties of the user object."""
|
||||
u = self.__loadUserSearch()[0]
|
||||
self.assertEquals("Dustin Sallings", u.fullname)
|
||||
self.assertEquals("dustin", u.name)
|
||||
self.assertEquals("dustin@spy.net", u.email)
|
||||
self.assertEquals("Santa Clara, CA", u.location)
|
||||
self.assertEquals("Ruby", u.language)
|
||||
self.assertEquals(35, u.actions)
|
||||
self.assertEquals(77, u.repos)
|
||||
self.assertEquals(78, u.followers)
|
||||
self.assertEquals('user-1779', u.id)
|
||||
self.assertAlmostEquals(12.231684, u.score)
|
||||
self.assertEquals('user', u.type)
|
||||
self.assertEquals('2008-02-29T17:59:09Z', u.created)
|
||||
self.assertEquals('2009-03-19T09:15:24.663Z', u.pushed)
|
||||
self.assertEquals("<<User dustin>>", repr(u))
|
||||
|
||||
def testUserPublic(self):
|
||||
"""Test the user show API with no authentication."""
|
||||
u = self.__loadUser('user.public.xml')
|
||||
self.assertEquals("Dustin Sallings", u.name)
|
||||
# self.assertEquals(None, u.company)
|
||||
self.assertEquals(10, u.following_count)
|
||||
self.assertEquals(21, u.public_gist_count)
|
||||
self.assertEquals(81, u.public_repo_count)
|
||||
self.assertEquals('http://bleu.west.spy.net/~dustin/', u.blog)
|
||||
self.assertEquals(1779, u.id)
|
||||
self.assertEquals(82, u.followers_count)
|
||||
self.assertEquals('dustin', u.login)
|
||||
self.assertEquals('Santa Clara, CA', u.location)
|
||||
self.assertEquals('dustin@spy.net', u.email)
|
||||
self.assertEquals('2008-02-29T09:59:09-08:00', u.created_at)
|
||||
|
||||
def testUserPrivate(self):
|
||||
"""Test the user show API with extra info from auth."""
|
||||
u = self.__loadUser('user.private.xml', 'dustin', 'blahblah')
|
||||
self.assertEquals("Dustin Sallings", u.name)
|
||||
# self.assertEquals(None, u.company)
|
||||
self.assertEquals(10, u.following_count)
|
||||
self.assertEquals(21, u.public_gist_count)
|
||||
self.assertEquals(81, u.public_repo_count)
|
||||
self.assertEquals('http://bleu.west.spy.net/~dustin/', u.blog)
|
||||
self.assertEquals(1779, u.id)
|
||||
self.assertEquals(82, u.followers_count)
|
||||
self.assertEquals('dustin', u.login)
|
||||
self.assertEquals('Santa Clara, CA', u.location)
|
||||
self.assertEquals('dustin@spy.net', u.email)
|
||||
self.assertEquals('2008-02-29T09:59:09-08:00', u.created_at)
|
||||
|
||||
# Begin private data
|
||||
|
||||
self.assertEquals("micro", u.plan.name)
|
||||
self.assertEquals(1, u.plan.collaborators)
|
||||
self.assertEquals(614400, u.plan.space)
|
||||
self.assertEquals(5, u.plan.private_repos)
|
||||
self.assertEquals(155191, u.disk_usage)
|
||||
self.assertEquals(6, u.collaborators)
|
||||
self.assertEquals(4, u.owned_private_repo_count)
|
||||
self.assertEquals(5, u.total_private_repo_count)
|
||||
self.assertEquals(0, u.private_gist_count)
|
||||
|
||||
def testKeysList(self):
|
||||
"""Test key listing."""
|
||||
kl = self._agh('http://github.com/api/v2/xml/user/keys?login=dustin&token=blahblah',
|
||||
'dustin', 'blahblah', 'data/keys.xml').users.keys()
|
||||
self.assertEquals(7, len(kl))
|
||||
k = kl[0]
|
||||
|
||||
self.assertEquals('some key', k.title)
|
||||
self.assertEquals(2181, k.id)
|
||||
self.assertEquals(549, k.key.find('cdEXwCSjAIFp8iRqh3GOkxGyFSc25qv/MuOBg=='))
|
||||
|
||||
def testRemoveKey(self):
|
||||
"""Remove a key."""
|
||||
self._ghp('user/key/remove',
|
||||
'dustin', 'p', id=828).users.removeKey(828)
|
||||
|
||||
def testAddKey(self):
|
||||
"""Add a key."""
|
||||
self._ghp('user/key/add',
|
||||
'dustin', 'p', name='my key', key='some key').users.addKey(
|
||||
'my key', 'some key')
|
||||
|
||||
class RepoTest(BaseCase):
|
||||
|
||||
def __loadUserRepos(self):
|
||||
return self._gh('http://github.com/api/v2/xml/repos/show/verbal',
|
||||
'data/repos.xml').repos.forUser('verbal')
|
||||
|
||||
def testUserRepoList(self):
|
||||
"""Get a list of repos for a user."""
|
||||
rs = self.__loadUserRepos()
|
||||
self.assertEquals(10, len(rs))
|
||||
r = rs[0]
|
||||
self.assertEquals('A beanstalk client for the twisted network framework.',
|
||||
r.description)
|
||||
self.assertEquals(2, r.watchers)
|
||||
self.assertEquals(0, r.forks)
|
||||
self.assertEquals('beanstalk-client-twisted', r.name)
|
||||
self.assertEquals(False, r.private)
|
||||
self.assertEquals('http://github.com/verbal/beanstalk-client-twisted',
|
||||
r.url)
|
||||
self.assertEquals(True, r.fork)
|
||||
self.assertEquals('verbal', r.owner)
|
||||
# XXX: Can't parse empty elements. :(
|
||||
# self.assertEquals('', r.homepage)
|
||||
|
||||
def testRepoSearch(self):
|
||||
"""Test searching a repository."""
|
||||
rl = self._gh('http://github.com/api/v2/xml/repos/search/ruby+testing',
|
||||
'data/repos.search.xml').repos.search('ruby testing')
|
||||
self.assertEquals(12, len(rl))
|
||||
|
||||
r = rl[0]
|
||||
self.assertEquals('synthesis', r.name)
|
||||
self.assertAlmostEquals(0.3234576, r.score, 4)
|
||||
self.assertEquals(4656, r.actions)
|
||||
self.assertEquals(2048, r.size)
|
||||
self.assertEquals('Ruby', r.language)
|
||||
self.assertEquals(26, r.followers)
|
||||
self.assertEquals('gmalamid', r.username)
|
||||
self.assertEquals('repo', r.type)
|
||||
self.assertEquals('repo-3555', r.id)
|
||||
self.assertEquals(1, r.forks)
|
||||
self.assertFalse(r.fork)
|
||||
self.assertEquals('Ruby test code analysis tool employing a '
|
||||
'"Synthesized Testing" strategy, aimed to reduce '
|
||||
'the volume of slower, coupled, complex wired tests.',
|
||||
r.description)
|
||||
self.assertEquals('2009-01-08T13:45:06Z', r.pushed)
|
||||
self.assertEquals('2008-03-11T23:38:04Z', r.created)
|
||||
|
||||
def testBranchList(self):
|
||||
"""Test branch listing for a repo."""
|
||||
bl = self._gh('http://github.com/api/v2/xml/repos/show/schacon/ruby-git/branches',
|
||||
'data/repos.branches.xml').repos.branches('schacon', 'ruby-git')
|
||||
self.assertEquals(4, len(bl))
|
||||
self.assertEquals('ee90922f3da3f67ef19853a0759c1d09860fe3b3', bl['master'])
|
||||
|
||||
def testGetOneRepo(self):
|
||||
"""Fetch an individual repository."""
|
||||
r = self._gh('http://github.com/api/v2/xml/repos/show/schacon/grit',
|
||||
'data/repo.xml').repos.show('schacon', 'grit')
|
||||
|
||||
self.assertEquals('Grit is a Ruby library for extracting information from a '
|
||||
'git repository in an object oriented manner - this fork '
|
||||
'tries to intergrate as much pure-ruby functionality as possible',
|
||||
r.description)
|
||||
self.assertEquals(68, r.watchers)
|
||||
self.assertEquals(4, r.forks)
|
||||
self.assertEquals('grit', r.name)
|
||||
self.assertFalse(r.private)
|
||||
self.assertEquals('http://github.com/schacon/grit', r.url)
|
||||
self.assertTrue(r.fork)
|
||||
self.assertEquals('schacon', r.owner)
|
||||
self.assertEquals('http://grit.rubyforge.org/', r.homepage)
|
||||
|
||||
def testGetRepoNetwork(self):
|
||||
"""Test network fetching."""
|
||||
nl = self._gh('http://github.com/api/v2/xml/repos/show/dustin/py-github/network',
|
||||
'data/network.xml').repos.network('dustin', 'py-github')
|
||||
self.assertEquals(5, len(nl))
|
||||
|
||||
n = nl[0]
|
||||
self.assertEquals('Python interface for talking to the github API',
|
||||
n.description)
|
||||
self.assertEquals('py-github', n.name)
|
||||
self.assertFalse(n.private)
|
||||
self.assertEquals('http://github.com/dustin/py-github', n.url)
|
||||
self.assertEquals(30, n.watchers)
|
||||
self.assertEquals(4, n.forks)
|
||||
self.assertFalse(n.fork)
|
||||
self.assertEquals('dustin', n.owner)
|
||||
self.assertEquals('http://dustin.github.com/2008/12/29/github-sync.html',
|
||||
n.homepage)
|
||||
|
||||
def testSetPublic(self):
|
||||
"""Test setting a repo visible."""
|
||||
self._ghp('repos/set/public/py-github', 'dustin', 'p').repos.setVisible(
|
||||
'py-github')
|
||||
|
||||
def testSetPrivate(self):
|
||||
"""Test setting a repo to private."""
|
||||
self._ghp('repos/set/private/py-github', 'dustin', 'p').repos.setVisible(
|
||||
'py-github', False)
|
||||
|
||||
def testCreateRepository(self):
|
||||
"""Test creating a repository."""
|
||||
self._ghp('repos/create', 'dustin', 'p',
|
||||
name='testrepo',
|
||||
description='woo',
|
||||
homepage='',
|
||||
public='1').repos.create(
|
||||
'testrepo', description='woo')
|
||||
|
||||
def testDeleteRepo(self):
|
||||
"""Test setting a repo to private."""
|
||||
self._ghp('repos/delete/mytest', 'dustin', 'p').repos.delete('mytest')
|
||||
|
||||
def testFork(self):
|
||||
"""Test forking'"""
|
||||
self._ghp('repos/fork/someuser/somerepo', 'dustin', 'p').repos.fork(
|
||||
'someuser', 'somerepo')
|
||||
|
||||
def testAddCollaborator(self):
|
||||
"""Adding a collaborator."""
|
||||
self._ghp('repos/collaborators/memcached/add/trondn',
|
||||
'dustin', 'p').repos.addCollaborator('memcached', 'trondn')
|
||||
|
||||
def testRemoveCollaborator(self):
|
||||
"""Removing a collaborator."""
|
||||
self._ghp('repos/collaborators/memcached/remove/trondn',
|
||||
'dustin', 'p').repos.removeCollaborator('memcached', 'trondn')
|
||||
|
||||
def testAddDeployKey(self):
|
||||
"""Add a deploy key."""
|
||||
self._ghp('repos/key/blah/add', 'dustin', 'p',
|
||||
title='title', key='key').repos.addDeployKey('blah', 'title', 'key')
|
||||
|
||||
def testRemoveDeployKey(self):
|
||||
"""Remove a deploy key."""
|
||||
self._ghp('repos/key/blah/remove', 'dustin', 'p',
|
||||
id=5).repos.removeDeployKey('blah', 5)
|
||||
|
||||
class CommitTest(BaseCase):
|
||||
|
||||
def testCommitList(self):
|
||||
"""Test commit list."""
|
||||
cl = self._gh('http://github.com/api/v2/xml/commits/list/mojombo/grit/master',
|
||||
'data/commits.xml').commits.forBranch('mojombo', 'grit')
|
||||
self.assertEquals(30, len(cl))
|
||||
|
||||
c = cl[0]
|
||||
self.assertEquals("Regenerated gemspec for version 1.1.1", c.message)
|
||||
self.assertEquals('4ac4acab7fd9c7fd4c0e0f4ff5794b0347baecde', c.id)
|
||||
self.assertEquals('94490563ebaf733cbb3de4ad659eb58178c2e574', c.tree)
|
||||
self.assertEquals('2009-03-31T09:54:51-07:00', c.committed_date)
|
||||
self.assertEquals('2009-03-31T09:54:51-07:00', c.authored_date)
|
||||
self.assertEquals('http://github.com/mojombo/grit/commit/4ac4acab7fd9c7fd4c0e0f4ff5794b0347baecde',
|
||||
c.url)
|
||||
self.assertEquals(1, len(c.parents))
|
||||
self.assertEquals('5071bf9fbfb81778c456d62e111440fdc776f76c', c.parents[0].id)
|
||||
self.assertEquals('Tom Preston-Werner', c.author.name)
|
||||
self.assertEquals('tom@mojombo.com', c.author.email)
|
||||
self.assertEquals('Tom Preston-Werner', c.committer.name)
|
||||
self.assertEquals('tom@mojombo.com', c.committer.email)
|
||||
|
||||
def testCommitListForFile(self):
|
||||
"""Test commit list for a file."""
|
||||
cl = self._gh('http://github.com/api/v2/xml/commits/list/mojombo/grit/master/grit.gemspec',
|
||||
'data/commits.xml').commits.forFile('mojombo', 'grit', 'grit.gemspec')
|
||||
self.assertEquals(30, len(cl))
|
||||
|
||||
c = cl[0]
|
||||
self.assertEquals("Regenerated gemspec for version 1.1.1", c.message)
|
||||
self.assertEquals('4ac4acab7fd9c7fd4c0e0f4ff5794b0347baecde', c.id)
|
||||
self.assertEquals('94490563ebaf733cbb3de4ad659eb58178c2e574', c.tree)
|
||||
self.assertEquals('2009-03-31T09:54:51-07:00', c.committed_date)
|
||||
self.assertEquals('2009-03-31T09:54:51-07:00', c.authored_date)
|
||||
self.assertEquals('http://github.com/mojombo/grit/commit/4ac4acab7fd9c7fd4c0e0f4ff5794b0347baecde',
|
||||
c.url)
|
||||
self.assertEquals(1, len(c.parents))
|
||||
self.assertEquals('5071bf9fbfb81778c456d62e111440fdc776f76c', c.parents[0].id)
|
||||
self.assertEquals('Tom Preston-Werner', c.author.name)
|
||||
self.assertEquals('tom@mojombo.com', c.author.email)
|
||||
self.assertEquals('Tom Preston-Werner', c.committer.name)
|
||||
self.assertEquals('tom@mojombo.com', c.committer.email)
|
||||
|
||||
def testIndividualCommit(self):
|
||||
"""Grab a single commit."""
|
||||
h = '4c86fa592fcc7cb685c6e9d8b6aebe8dcbac6b3e'
|
||||
c = self._gh('http://github.com/api/v2/xml/commits/show/dustin/memcached/' + h,
|
||||
'data/commit.xml').commits.show('dustin', 'memcached', h)
|
||||
self.assertEquals(['internal_tests.c'], c.removed)
|
||||
self.assertEquals(set(['cache.c', 'cache.h', 'testapp.c']), set(c.added))
|
||||
self.assertEquals('Create a generic cache for objects of same size\n\n'
|
||||
'The suffix pool could be thread-local and use the generic cache',
|
||||
c.message)
|
||||
|
||||
self.assertEquals(6, len(c.modified))
|
||||
self.assertEquals('.gitignore', c.modified[0].filename)
|
||||
self.assertEquals(140, len(c.modified[0].diff))
|
||||
|
||||
self.assertEquals(['ee0c3d5ae74d0862b4d9990e2ad13bc79f8c34df'],
|
||||
[p.id for p in c.parents])
|
||||
self.assertEquals('http://github.com/dustin/memcached/commit/' + h, c.url)
|
||||
self.assertEquals('Trond Norbye', c.author.name)
|
||||
self.assertEquals('Trond.Norbye@sun.com', c.author.email)
|
||||
self.assertEquals(h, c.id)
|
||||
self.assertEquals('2009-04-17T16:15:52-07:00', c.committed_date)
|
||||
self.assertEquals('2009-03-27T10:30:16-07:00', c.authored_date)
|
||||
self.assertEquals('94b644163f6381a9930e2d7c583fae023895b903', c.tree)
|
||||
self.assertEquals('Dustin Sallings', c.committer.name)
|
||||
self.assertEquals('dustin@spy.net', c.committer.email)
|
||||
|
||||
def testWatchRepo(self):
|
||||
"""Test watching a repo."""
|
||||
self._ghp('repos/watch/dustin/py-github', 'dustin', 'p').repos.watch(
|
||||
'dustin', 'py-github')
|
||||
|
||||
def testWatchRepo(self):
|
||||
"""Test watching a repo."""
|
||||
self._ghp('repos/unwatch/dustin/py-github', 'dustin', 'p').repos.unwatch(
|
||||
'dustin', 'py-github')
|
||||
|
||||
class IssueTest(BaseCase):
|
||||
|
||||
def testListIssues(self):
|
||||
"""Test listing issues."""
|
||||
il = self._gh('http://github.com/api/v2/xml/issues/list/schacon/simplegit/open',
|
||||
'data/issues.list.xml').issues.list('schacon', 'simplegit')
|
||||
self.assertEquals(1, len(il))
|
||||
i = il[0]
|
||||
|
||||
self.assertEquals('schacon', i.user)
|
||||
self.assertEquals('2009-04-17T16:19:02-07:00', i.updated_at)
|
||||
self.assertEquals('something', i.body)
|
||||
self.assertEquals('new', i.title)
|
||||
self.assertEquals(2, i.number)
|
||||
self.assertEquals(0, i.votes)
|
||||
self.assertEquals(1.0, i.position)
|
||||
self.assertEquals('2009-04-17T16:18:50-07:00', i.created_at)
|
||||
self.assertEquals('open', i.state)
|
||||
|
||||
def testShowIssue(self):
|
||||
"""Show an individual issue."""
|
||||
i = self._gh('http://github.com/api/v2/xml/issues/show/dustin/py-github/1',
|
||||
'data/issues.show.xml').issues.show('dustin', 'py-github', 1)
|
||||
|
||||
self.assertEquals('dustin', i.user)
|
||||
self.assertEquals('2009-04-17T18:37:04-07:00', i.updated_at)
|
||||
self.assertEquals('http://develop.github.com/p/general.html', i.body)
|
||||
self.assertEquals('Add auth tokens', i.title)
|
||||
self.assertEquals(1, i.number)
|
||||
self.assertEquals(0, i.votes)
|
||||
self.assertEquals(1.0, i.position)
|
||||
self.assertEquals('2009-04-17T17:00:58-07:00', i.created_at)
|
||||
self.assertEquals('closed', i.state)
|
||||
|
||||
def testAddLabel(self):
|
||||
"""Adding a label to an issue."""
|
||||
self._ghp('issues/label/add/dustin/py-github/todo/33', 'd', 'pw').issues.add_label(
|
||||
'dustin', 'py-github', 33, 'todo')
|
||||
|
||||
def testRemoveLabel(self):
|
||||
"""Removing a label from an issue."""
|
||||
self._ghp('issues/label/remove/dustin/py-github/todo/33',
|
||||
'd', 'pw').issues.remove_label(
|
||||
'dustin', 'py-github', 33, 'todo')
|
||||
|
||||
def testCloseIssue(self):
|
||||
"""Closing an issue."""
|
||||
self._ghp('issues/close/dustin/py-github/1', 'd', 'pw').issues.close(
|
||||
'dustin', 'py-github', 1)
|
||||
|
||||
def testReopenIssue(self):
|
||||
"""Reopening an issue."""
|
||||
self._ghp('issues/reopen/dustin/py-github/1', 'd', 'pw').issues.reopen(
|
||||
'dustin', 'py-github', 1)
|
||||
|
||||
def testCreateIssue(self):
|
||||
"""Creating an issue."""
|
||||
self._ghp('issues/open/dustin/py-github', 'd', 'pw',
|
||||
title='test title', body='').issues.new(
|
||||
'dustin', 'py-github', title='test title')
|
||||
|
||||
def testEditIssue(self):
|
||||
"""Editing an existing issue."""
|
||||
self._ghp('issues/edit/dustin/py-github/1', 'd', 'pw',
|
||||
title='new title', body='new body').issues.edit(
|
||||
'dustin', 'py-github', 1, 'new title', 'new body')
|
||||
|
||||
class ObjectTest(BaseCase):
|
||||
|
||||
def testTree(self):
|
||||
"""Test tree fetching."""
|
||||
h = '1ddd3f99f0b96019042239375b3ad4d45796ffba'
|
||||
tl = self._gh('http://github.com/api/v2/xml/tree/show/dustin/py-github/' + h,
|
||||
'data/tree.xml').objects.tree('dustin', 'py-github', h)
|
||||
self.assertEquals(8, len(tl))
|
||||
self.assertEquals('setup.py', tl['setup.py'].name)
|
||||
self.assertEquals('6e290379ec58fa00ac9d1c2a78f0819a21397445',
|
||||
tl['setup.py'].sha)
|
||||
self.assertEquals('100755', tl['setup.py'].mode)
|
||||
self.assertEquals('blob', tl['setup.py'].type)
|
||||
|
||||
self.assertEquals('src', tl['src'].name)
|
||||
self.assertEquals('5fb9175803334c82b3fd66f1b69502691b91cf4f',
|
||||
tl['src'].sha)
|
||||
self.assertEquals('040000', tl['src'].mode)
|
||||
self.assertEquals('tree', tl['src'].type)
|
||||
|
||||
def testBlob(self):
|
||||
"""Test blob fetching."""
|
||||
h = '1ddd3f99f0b96019042239375b3ad4d45796ffba'
|
||||
blob = self._gh('http://github.com/api/v2/xml/blob/show/dustin/py-github/'
|
||||
+ h + '/setup.py',
|
||||
'data/blob.xml').objects.blob('dustin', 'py-github', h, 'setup.py')
|
||||
self.assertEquals('setup.py', blob.name)
|
||||
self.assertEquals(1842, blob.size)
|
||||
self.assertEquals('6e290379ec58fa00ac9d1c2a78f0819a21397445', blob.sha)
|
||||
self.assertEquals('100755', blob.mode)
|
||||
self.assertEquals('text/plain', blob.mime_type)
|
||||
self.assertEquals(1842, len(blob.data))
|
||||
self.assertEquals(1641, blob.data.index('Production/Stable'))
|
||||
|
||||
def testRawBlob(self):
|
||||
"""Test raw blob fetching."""
|
||||
h = '6e290379ec58fa00ac9d1c2a78f0819a21397445'
|
||||
blob = self._gh('http://github.com/api/v2/xml/blob/show/dustin/py-github/' + h,
|
||||
'data/setup.py').objects.raw_blob('dustin', 'py-github', h)
|
||||
self.assertEquals('e2dc8aea9ae8961f4f5923f9febfdd0a',
|
||||
hashlib.md5(blob).hexdigest())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user