[ADD] oe: provides sane (unfucked) command-line interface.

The implementation is far from perfect. Some improvements are waiting in
its previous location: lp:~openerp/openerp-command.

Some docs are provided, see doc/openerp-command.rst and
doc/adding-command.rst.

bzr revid: vmt@openerp.com-20130111134657-im2f3uqjluyo4pm6
This commit is contained in:
Vo Minh Thu 2013-01-11 14:46:57 +01:00
parent 3478333bca
commit 3da57500c2
25 changed files with 1554 additions and 0 deletions

34
doc/adding-command.rst Normal file
View File

@ -0,0 +1,34 @@
.. _adding-command:
Adding a new command
====================
``oe`` uses the argparse_ library to implement commands. Each
command lives in its own ``openerpcommand/<command>.py`` file.
.. _argparse: http://docs.python.org/2.7/library/argparse.html
To create a new command, probably the most simple way to get started is to
copy/paste an existing command, say ``openerpcommand/initialize.py`` to
``openerpcommand/foo.py``. In the newly created file, the important bits
are the ``run(args)`` and ``add_parser(subparsers)`` functions.
``add_parser``'s responsability is to create a (sub-)parser for the command,
i.e. describe the different options and flags. The last thing it does is to set
``run`` as the function to call when the command is invoked.
.. code-block:: python
> def add_parser(subparsers):
> parser = subparsers.add_parser('<command-name>',
> description='...')
> parser.add_argument(...)
> ...
> parser.set_defaults(run=run)
``run(args)`` actually implements the command. It should be kept as simple as
possible and delegate most of its work to small functions (probably placed at
the top of the new file). In other words, its responsability is mainly to
deal with the presence/absence/pre-processing of ``argparse``'s arguments.
Finally, the module must be added to ``openerpcommand/__init__.py``.

View File

@ -18,6 +18,15 @@ OpenERP Server
06_misc
09_deployment
OpenERP Command
'''''''''''''''
.. toctree::
:maxdepth: 1
openerp-command.rst
adding-command.rst
OpenERP Server API
''''''''''''''''''

60
doc/openerp-command.rst Normal file
View File

@ -0,0 +1,60 @@
.. _openerp-command:
OpenERP Command
===============
The ``oe`` script provides a set of command-line tools around the OpenERP
framework.
Using OpenERP Command
---------------------
In contrast to the previous ``openerp-server`` script, ``oe`` defines a few
sub-commands, each with its own set of flags and options. You can get some
information for any of them with
::
> oe <sub-command> --help
For instance::
> oe run-tests --help
Some ``oe`` options can be provided via environment variables. For instance::
> export OPENERP_DATABASE=trunk
> export OPENERP_HOST=127.0.0.1
> export OPENERP_PORT=8069
Depending on your needs, you can group all of the above in one single script;
for instance here is a, say, ``test-trunk-view-validation.sh`` file::
COMMAND_REPO=/home/thu/repos/command/trunk/
SERVER_REPO=/home/thu/repos/server/trunk
export PYTHONPATH=$SERVER_REPO:$COMMAND_REPO
export PATH=$SERVER_REPO:$COMMAND_REPO:$PATH
export OPENERP_DATABASE=trunk
export OPENERP_HOST=127.0.0.1
export OPENERP_PORT=8069
# The -d ignored is actually needed by `oe` even though `test_view_validation`
# itself does not need it.
oe run-tests -d ignored -m openerp.test_view_validation
Adding new commands
-------------------
See the :doc:`adding-command` page.
Bash completion
---------------
A preliminary ``oe-bash-completion`` file is provided. After sourcing it,
::
> . oe-bash-completion
completion (using the TAB character) in Bash should be working.

5
oe Executable file
View File

@ -0,0 +1,5 @@
#! /usr/bin/env python2
if __name__ == '__main__':
import openerpcommand.main
openerpcommand.main.run()

89
oe-bash-completion Normal file
View File

@ -0,0 +1,89 @@
_oe()
{
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
cmd="${COMP_WORDS[0]}"
subcmd=""
if [[ ${COMP_CWORD} > 0 ]] ; then
subcmd="${COMP_WORDS[1]}"
fi
# oe
opts="initialize model read run-tests scaffold update \
call open show consume-nothing consume-memory leak-memory \
consume-cpu bench-read bench-fields-view-get bench-dummy bench-login \
bench-sale-mrp --help"
if [[ ${prev} == oe && ${cur} != -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# oe call
opts="--database --user --password --host --port --help"
if [[ ${subcmd} == call ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# oe initialize
opts="--database --addons --all-modules --exclude --no-create --help"
if [[ ${subcmd} == initialize ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# oe model
opts="--database --model --field --verbose --help"
if [[ ${subcmd} == model ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# oe read
opts="--database --model --id --field --verbose --short --help"
if [[ ${subcmd} == read ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# oe run-tests
opts="--database --addons --module --dry-run --help"
if [[ ${subcmd} == run-tests ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# oe scaffold
opts="--help"
if [[ ${subcmd} == scaffold ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# fallback for unimplemented completion
opts="--help"
if [[ true ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
}
complete -F _oe oe

View File

@ -0,0 +1,61 @@
import argparse
import textwrap
from .call import Call
from .client import Open, Show, ConsumeNothing, ConsumeMemory, LeakMemory, ConsumeCPU
from .benchmarks import Bench, BenchRead, BenchFieldsViewGet, BenchDummy, BenchLogin
from .bench_sale_mrp import BenchSaleMrp
from . import common
from . import conf # Not really server-side (in the `for` below).
from . import drop
from . import initialize
from . import model
from . import module
from . import read
from . import run_tests
from . import scaffold
from . import uninstall
from . import update
command_list_server = (conf, drop, initialize, model, module, read, run_tests,
scaffold, uninstall, update, )
command_list_client = (Call, Open, Show, ConsumeNothing, ConsumeMemory,
LeakMemory, ConsumeCPU, Bench, BenchRead,
BenchFieldsViewGet, BenchDummy, BenchLogin,
BenchSaleMrp, )
def main_parser():
parser = argparse.ArgumentParser(
usage=argparse.SUPPRESS,
description=textwrap.fill(textwrap.dedent("""\
OpenERP Command provides a set of command-line tools around
the OpenERP framework: openobject-server. All the tools are
sub-commands of a single oe executable.""")),
epilog="""Use <command> --help to get information about the command.""",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
description = []
for x in command_list_server:
description.append(x.__name__[len(__package__)+1:])
if x.__doc__:
description.extend([
":\n",
textwrap.fill(str(x.__doc__).strip(),
subsequent_indent=' ',
initial_indent=' '),
])
description.append("\n\n")
subparsers = parser.add_subparsers(
title="Available commands",
help=argparse.SUPPRESS,
description="".join(description[:-1]),
)
# Server-side commands.
for x in command_list_server:
x.add_parser(subparsers)
# Client-side commands. TODO one per .py file.
for x in command_list_client:
x(subparsers)
return parser

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
# Nothing here, the module provides only data.
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
{
'name': 'bench_sale_mrp',
'version': '0.1',
'category': 'Benchmarks',
'description': """Prepare some data to run a benchmark.""",
'author': 'OpenERP SA',
'maintainer': 'OpenERP SA',
'website': 'http://www.openerp.com',
'depends': ['base', 'sale_mrp'],
'data': ['data.yml'],
'installable': True,
'auto_install': False,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,41 @@
-
This is a subset of `sale_mrp/test/sale_mrp.yml`.
-
I define a product category `Mobile Products Sellable`.
-
!record {model: product.category, id: my_product_category_0}:
name: Mobile Products Sellable
-
I define a product `Slider Mobile`
-
!record {model: product.product, id: my_slider_mobile_0}:
categ_id: my_product_category_0
cost_method: standard
list_price: 200.0
mes_type: fixed
name: Slider Mobile
procure_method: make_to_order
seller_delay: '1'
seller_ids:
- delay: 1
name: base.res_partner_agrolait
min_qty: 2.0
qty: 5.0
standard_price: 189.0
supply_method: produce
type: product
uom_id: product.product_uom_unit
uom_po_id: product.product_uom_unit
-
I create a Bill of Material for the `Slider Mobile` product.
-
!record {model: mrp.bom, id: mrp_bom_slidermobile0}:
company_id: base.main_company
name: Slider Mobile
product_efficiency: 1.0
product_id: my_slider_mobile_0
product_qty: 1.0
product_uom: product.product_uom_unit
product_uos_qty: 0.0
sequence: 0.0
type: normal

View File

@ -0,0 +1,68 @@
"""
Benchmark based on the `sale_mrp` addons (in `sale_mrp/test/sale_mrp.yml`).
"""
import time
from .benchmarks import Bench
class BenchSaleMrp(Bench):
"""\
Similar to `sale_mrp/test/sale_mrp.yml`.
This benchmarks the OpenERP server `sale_mrp` module by creating and
confirming a sale order. As it creates data in the server, it is necessary
to ensure unique names for the newly created data. You can use the --seed
argument to give a lower bound to those names. (The number of generated
names is --jobs * --samples.)
"""
command_name = 'bench-sale-mrp'
bench_name = '`sale_mrp/test/sale_mrp.yml`'
def measure_once(self, i):
if self.worker >= 0:
i = int(self.args.seed) + i + (self.worker * int(self.args.samples))
else:
i = int(self.args.seed) + i
# Resolve a few external-ids (this has little impact on the running
# time of the whole method).
product_uom_unit = self.execute('ir.model.data', 'get_object_reference', 'product', 'product_uom_unit')[1]
my_slider_mobile_0 = self.execute('ir.model.data', 'get_object_reference', 'bench_sale_mrp', 'my_slider_mobile_0')[1]
res_partner_4 = self.execute('ir.model.data', 'get_object_reference', 'base', 'res_partner_4')[1]
res_partner_address_7 = self.execute('ir.model.data', 'get_object_reference', 'base', 'res_partner_address_7')[1]
list0 = self.execute('ir.model.data', 'get_object_reference', 'product', 'list0')[1]
shop = self.execute('ir.model.data', 'get_object_reference', 'sale', 'shop')[1]
# Create a sale order for the product `Slider Mobile`.
data = {
'client_order_ref': 'ref_xxx_' + str(i).rjust(6, '0'),
'date_order': time.strftime('%Y-%m-%d'),
'invoice_quantity': 'order',
'name': 'sale_order_ref_xxx_' + str(i).rjust(6, '0'),
'order_line': [(0, 0, {
'name': 'Slider Mobile',
'price_unit': 2,
'product_uom': product_uom_unit,
'product_uom_qty': 5.0,
'state': 'draft',
'delay': 7.0,
'product_id': my_slider_mobile_0,
'product_uos_qty': 5,
'type': 'make_to_order',
})],
'order_policy': 'manual',
'partner_id': res_partner_4,
'partner_invoice_id': res_partner_address_7,
'partner_order_id': res_partner_address_7,
'partner_shipping_id': res_partner_address_7,
'picking_policy': 'direct',
'pricelist_id': list0,
'shop_id': shop,
}
sale_order_id = self.execute('sale.order', 'create', data, {})
# Confirm the sale order.
self.object_proxy.exec_workflow(self.database, self.uid, self.password, 'sale.order', 'order_confirm', sale_order_id, {})

View File

@ -0,0 +1,166 @@
"""
Define a base class for client-side benchmarking.
"""
import hashlib
import multiprocessing
import sys
import time
from .client import Client
class Bench(Client):
"""
Base class for concurrent benchmarks. The measure_once() method must be
overriden.
Each sub-benchmark will be run in its own process then a report is done
with all the results (shared with the main process using a
`multiprocessing.Array`).
"""
def __init__(self, subparsers=None):
super(Bench, self).__init__(subparsers)
self.parser.add_argument('-n', '--samples', metavar='INT',
default=100, help='number of measurements to take')
# TODO if -n <int>s is given (instead of -n <int>), run the
# benchmark for <int> seconds and return the number of iterations.
self.parser.add_argument('-o', '--output', metavar='PATH',
required=True, help='path to save the generated report')
self.parser.add_argument('--append', action='store_true',
default=False, help='append the report to an existing file')
self.parser.add_argument('-j', '--jobs', metavar='JOBS',
default=1, help='number of concurrent workers')
self.parser.add_argument('--seed', metavar='SEED',
default=0, help='a value to ensure different runs can create unique data')
self.worker = -1
def work(self, iarr=None):
if iarr:
# If an array is given, it means we are a worker process...
self.work_slave(iarr)
else:
# ... else we are the main process and we will spawn workers,
# passing them an array.
self.work_master()
def work_master(self):
N = int(self.args.samples)
self.arrs = [(i, multiprocessing.Array('f', range(N)))
for i in xrange(int(self.args.jobs))]
ps = [multiprocessing.Process(target=self.run, args=(arr,))
for arr in self.arrs]
[p.start() for p in ps]
[p.join() for p in ps]
self.report_html()
def work_slave(self, iarr):
j, arr = iarr
self.worker = j
N = int(self.args.samples)
total_t0 = time.time()
for i in xrange(N):
t0 = time.time()
self.measure_once(i)
t1 = time.time()
arr[i] = t1 - t0
print >> sys.stdout, '\r%s' % ('|' * (i * 60 / N)),
print >> sys.stdout, '%s %s%%' % \
(' ' * (60 - (i * 60 / N)), int(float(i+1)/N*100)),
sys.stdout.flush()
total_t1 = time.time()
print '\nDone in %ss.' % (total_t1 - total_t0)
def report_html(self):
series = []
for arr in self.arrs:
serie = """{
data: %s,
points: { show: true }
}""" % ([[x, i] for i, x in enumerate(arr)],)
series.append(serie)
chart_id = hashlib.md5(" ".join(sys.argv)).hexdigest()
HEADER = """<!doctype html>
<title>Benchmarks</title>
<meta charset=utf-8>
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="js/jquery.flot.js"></script>
"""
CONTENT = """<h1>%s</h1>
%s
<div id='chart_%s' style='width:400px;height:300px;'>...</div>
<script type="text/javascript">
$.plot($("#chart_%s"), [%s],
{yaxis: { ticks: false }});
</script>""" % (self.bench_name, ' '.join(sys.argv), chart_id, chart_id,
','.join(series))
if self.args.append:
with open(self.args.output, 'a') as f:
f.write(CONTENT,)
else:
with open(self.args.output, 'w') as f:
f.write(HEADER + CONTENT,)
def measure_once(self, i):
"""
The `measure_once` method is called --jobs times. A `i` argument is
supplied to allow to create unique values for each execution (e.g. to
supply fresh identifiers to a `create` method.
"""
pass
class BenchRead(Bench):
"""Read a record repeatedly."""
command_name = 'bench-read'
bench_name = 'res.users.read(1)'
def __init__(self, subparsers=None):
super(BenchRead, self).__init__(subparsers)
self.parser.add_argument('-m', '--model', metavar='MODEL',
required=True, help='the model')
self.parser.add_argument('-i', '--id', metavar='RECORDID',
required=True, help='the record id')
def measure_once(self, i):
self.execute(self.args.model, 'read', [self.args.id], [])
class BenchFieldsViewGet(Bench):
"""Read a record's fields and view architecture repeatedly."""
command_name = 'bench-view'
bench_name = 'res.users.fields_view_get(1)'
def __init__(self, subparsers=None):
super(BenchFieldsViewGet, self).__init__(subparsers)
self.parser.add_argument('-m', '--model', metavar='MODEL',
required=True, help='the model')
self.parser.add_argument('-i', '--id', metavar='RECORDID',
required=True, help='the record id')
def measure_once(self, i):
self.execute(self.args.model, 'fields_view_get', self.args.id)
class BenchDummy(Bench):
"""Dummy (call test.limits.model.consume_nothing())."""
command_name = 'bench-dummy'
bench_name = 'test.limits.model.consume_nothing()'
def __init__(self, subparsers=None):
super(BenchDummy, self).__init__(subparsers)
self.parser.add_argument('-a', '--args', metavar='ARGS',
default='', help='some arguments to serialize')
def measure_once(self, i):
self.execute('test.limits.model', 'consume_nothing')
class BenchLogin(Bench):
"""Login (update res_users.date)."""
command_name = 'bench-login'
bench_name = 'res.users.login(1)'
def measure_once(self, i):
self.common_proxy.login(self.database, self.user, self.password)

44
openerpcommand/call.py Normal file
View File

@ -0,0 +1,44 @@
"""
Call an arbitrary model's method.
"""
import ast
import os
import pprint
import sys
import time
import xmlrpclib
import client
class Call(client.Client):
"""\
Call an arbitrary model's method.
Example:
> oe call res.users.read '[1, 3]' '[]' -u 1 -p admin
"""
# TODO The above docstring is completely borked in the
# --help message.
command_name = 'call'
def __init__(self, subparsers=None):
super(Call, self).__init__(subparsers)
self.parser.add_argument('call', metavar='MODEL.METHOD',
help='the model and the method to call, using the '
'<model>.<method> format.')
self.parser.add_argument('args', metavar='ARGUMENT',
nargs='+',
help='the argument for the method call, must be '
'`ast.literal_eval` compatible. Can be repeated.')
def work(self):
try:
model, method = self.args.call.rsplit('.', 1)
except:
print "Invalid syntax `%s` must have the form <model>.<method>."
sys.exit(1)
args = tuple(map(ast.literal_eval, self.args.args)) if self.args.args else ()
x = self.execute(model, method, *args)
pprint.pprint(x, indent=4)

137
openerpcommand/client.py Normal file
View File

@ -0,0 +1,137 @@
"""
Define a few common arguments for client-side command-line tools.
"""
import os
import sys
import time
import xmlrpclib
import common
class Client(common.Command):
"""
Base class for XML-RPC command-line clients. It must be inherited and the
work() method overriden.
"""
def __init__(self, subparsers=None):
super(Client, self).__init__(subparsers)
required_or_default = common.required_or_default
self.parser.add_argument('-H', '--host', metavar='HOST',
**required_or_default('HOST', 'the server host'))
self.parser.add_argument('-P', '--port', metavar='PORT',
**required_or_default('PORT', 'the server port'))
def execute(self, *args):
return self.object_proxy.execute(self.database, self.uid, self.password, *args)
def initialize(self):
self.host = self.args.host
self.port = int(self.args.port)
self.database = self.args.database
self.user = self.args.user
self.password = self.args.password
self.url = 'http://%s:%d/xmlrpc/' % (self.host, self.port)
self.common_proxy = xmlrpclib.ServerProxy(self.url + 'common')
self.object_proxy = xmlrpclib.ServerProxy(self.url + 'object')
try:
self.uid = int(self.user)
except ValueError, e:
self.uid = self.common_proxy.login(self.database, self.user, self.password)
def run(self, *args):
self.initialize()
self.work(*args)
def work(self, *args):
pass
class Open(Client):
"""Get the web client's URL to view a specific model."""
command_name = 'open'
def __init__(self, subparsers=None):
super(Open, self).__init__(subparsers)
self.parser.add_argument('-m', '--model', metavar='MODEL',
required=True, help='the view type')
self.parser.add_argument('-v', '--view-mode', metavar='VIEWMODE',
default='tree', help='the view mode')
def work(self):
ids = self.execute('ir.actions.act_window', 'search', [
('res_model', '=', self.args.model),
('view_mode', 'like', self.args.view_mode),
])
xs = self.execute('ir.actions.act_window', 'read', ids, [])
for x in xs:
print x['id'], x['name']
d = {}
d['host'] = self.host
d['port'] = self.port
d['action_id'] = x['id']
print " http://%(host)s:%(port)s/web/webclient/home#action_id=%(action_id)s" % d
class Show(Client):
"""Display a record."""
command_name = 'show'
def __init__(self, subparsers=None):
super(Show, self).__init__(subparsers)
self.parser.add_argument('-m', '--model', metavar='MODEL',
required=True, help='the model')
self.parser.add_argument('-i', '--id', metavar='RECORDID',
required=True, help='the record id')
def work(self):
xs = self.execute(self.args.model, 'read', [self.args.id], [])
if xs:
x = xs[0]
print x['name']
else:
print "Record not found."
class ConsumeNothing(Client):
"""Call test.limits.model.consume_nothing()."""
command_name = 'consume-nothing'
def work(self):
xs = self.execute('test.limits.model', 'consume_nothing')
class ConsumeMemory(Client):
"""Call test.limits.model.consume_memory()."""
command_name = 'consume-memory'
def __init__(self, subparsers=None):
super(ConsumeMemory, self).__init__(subparsers)
self.parser.add_argument('--size', metavar='SIZE',
required=True, help='size of the list to allocate')
def work(self):
xs = self.execute('test.limits.model', 'consume_memory', int(self.args.size))
class LeakMemory(ConsumeMemory):
"""Call test.limits.model.leak_memory()."""
command_name = 'leak-memory'
def work(self):
xs = self.execute('test.limits.model', 'leak_memory', int(self.args.size))
class ConsumeCPU(Client):
"""Call test.limits.model.consume_cpu_time()."""
command_name = 'consume-cpu'
def __init__(self, subparsers=None):
super(ConsumeCPU, self).__init__(subparsers)
self.parser.add_argument('--seconds', metavar='INT',
required=True, help='how much CPU time to consume')
def work(self):
xs = self.execute('test.limits.model', 'consume_cpu_time', int(self.args.seconds))

88
openerpcommand/common.py Normal file
View File

@ -0,0 +1,88 @@
"""
Define a few common arguments for server-side command-line tools.
"""
import argparse
import os
import sys
def add_addons_argument(parser):
"""
Add a common --addons argument to a parser.
"""
parser.add_argument('--addons', metavar='ADDONS',
**required_or_default('ADDONS',
'colon-separated list of paths to addons'))
def get_addons_from_paths(paths, exclude):
"""
Build a list of available modules from a list of addons paths.
"""
exclude = exclude or []
module_names = []
for p in paths:
if os.path.exists(p):
names = list(set(os.listdir(p)))
names = filter(lambda a: not (a.startswith('.') or a in exclude), names)
module_names.extend(names)
else:
print "The addons path `%s` doesn't exist." % p
sys.exit(1)
return module_names
def required_or_default(name, h):
"""
Helper to define `argparse` arguments. If the name is the environment,
the argument is optional and draw its value from the environment if not
supplied on the command-line. If it is not in the environment, make it
a mandatory argument.
"""
if os.environ.get('OPENERP_' + name.upper()):
d = {'default': os.environ['OPENERP_' + name.upper()]}
else:
d = {'required': True}
d['help'] = h + '. The environment variable OPENERP_' + \
name.upper() + ' can be used instead.'
return d
class Command(object):
"""
Base class to create command-line tools. It must be inherited and the
run() method overriden.
"""
command_name = 'stand-alone'
def __init__(self, subparsers=None):
if subparsers:
self.parser = parser = subparsers.add_parser(self.command_name,
description=self.__class__.__doc__)
else:
self.parser = parser = argparse.ArgumentParser(
description=self.__class__.__doc__)
parser.add_argument('-d', '--database', metavar='DATABASE',
**required_or_default('DATABASE', 'the database to connect to'))
parser.add_argument('-u', '--user', metavar='USER',
**required_or_default('USER', 'the user login or ID. When using '
'RPC, providing an ID avoid the login() step'))
parser.add_argument('-p', '--password', metavar='PASSWORD',
**required_or_default('PASSWORD', 'the user password')) # TODO read it from the command line or from file.
parser.set_defaults(run=self.run_with_args)
def run_with_args(self, args):
self.args = args
self.run()
def run(self):
print 'Stub Command.run().'
@classmethod
def stand_alone(cls):
"""
A single Command object is a complete command-line program. See
`openerp-command/stand-alone` for an example.
"""
command = cls()
args = command.parser.parse_args()
args.run(args)

25
openerpcommand/conf.py Normal file
View File

@ -0,0 +1,25 @@
"""
Display the currently used configuration. The configuration for any
sub-command is normally given by options. But some options can be specified
using environment variables. This sub-command shows those variables.
A `set` sub-command should be provided when the configuration is in a real
configuration file instead of environment variables.
"""
import os
import sys
import textwrap
def run(args):
for x in ('database', 'addons', 'host', 'port'):
x_ = ('openerp_' + x).upper()
if x_ in os.environ:
print '%s: %s' % (x, os.environ[x_])
else:
print '%s: <not set>' % (x, )
os.environ['OPENERP_DATABASE'] = 'yeah'
def add_parser(subparsers):
parser = subparsers.add_parser('conf',
description='Display the currently used configuration.')
parser.set_defaults(run=run)

45
openerpcommand/drop.py Normal file
View File

@ -0,0 +1,45 @@
"""
Drop a database.
"""
import common
# TODO turn template1 in a parameter
# This should be exposed from openerp (currently in
# openerp/service/web_services.py).
def drop_database(database_name):
import openerp
db = openerp.sql_db.db_connect('template1')
cr = db.cursor()
cr.autocommit(True) # avoid transaction block
try:
# TODO option for doing this.
# Try to terminate all other connections that might prevent
# dropping the database
try:
cr.execute("""SELECT pg_terminate_backend(procpid)
FROM pg_stat_activity
WHERE datname = %s AND
procpid != pg_backend_pid()""",
(database_name,))
except Exception:
pass
try:
cr.execute('DROP DATABASE "%s"' % database_name)
except Exception, e:
print "Can't drop %s" % (database_name,)
finally:
cr.close()
def run(args):
assert args.database
drop_database(args.database)
def add_parser(subparsers):
parser = subparsers.add_parser('drop',
description='Drop a database.')
parser.add_argument('-d', '--database', metavar='DATABASE',
**common.required_or_default('DATABASE', 'the database to create'))
parser.set_defaults(run=run)

View File

@ -0,0 +1,103 @@
"""
Install OpenERP on a new (by default) database.
"""
import os
import sys
import common
def install_openerp(database_name, create_database_flag, module_names, install_demo_data):
import openerp
config = openerp.tools.config
if create_database_flag:
create_database(database_name)
config['init'] = dict.fromkeys(module_names, 1)
# Install the import hook, to import openerp.addons.<module>.
openerp.modules.module.initialize_sys_path()
if hasattr(openerp.modules.loading, 'open_openerp_namespace'):
openerp.modules.loading.open_openerp_namespace()
registry = openerp.modules.registry.RegistryManager.get(
database_name, update_module=True, force_demo=install_demo_data)
return registry
# TODO turn template1 in a parameter
# This should be exposed from openerp (currently in
# openerp/service/web_services.py).
def create_database(database_name):
import openerp
db = openerp.sql_db.db_connect('template1')
cr = db.cursor() # TODO `with db as cr:`
try:
cr.autocommit(True)
cr.execute("""CREATE DATABASE "%s"
ENCODING 'unicode' TEMPLATE "template1" """ \
% (database_name,))
finally:
cr.close()
def run(args):
assert args.database
assert not (args.module and args.all_modules)
import openerp
config = openerp.tools.config
if args.tests:
config['log_handler'] = [':TEST']
config['test_enable'] = True
config['without_demo'] = False
else:
config['log_handler'] = [':CRITICAL']
config['test_enable'] = False
config['without_demo'] = True
if args.addons:
args.addons = args.addons.split(':')
else:
args.addons = []
config['addons_path'] = ','.join(args.addons)
if args.all_modules:
module_names = common.get_addons_from_paths(args.addons, args.exclude)
elif args.module:
module_names = args.module
else:
module_names = ['base']
openerp.netsvc.init_logger()
registry = install_openerp(args.database, not args.no_create, module_names, not config['without_demo'])
# The `_assertion_report` attribute was added on the registry during the
# OpenERP 7.0 development.
if hasattr(registry, '_assertion_report'):
sys.exit(1 if registry._assertion_report.failures else 0)
def add_parser(subparsers):
parser = subparsers.add_parser('initialize',
description='Create and initialize a new OpenERP database.')
parser.add_argument('-d', '--database', metavar='DATABASE',
**common.required_or_default('DATABASE', 'the database to create'))
common.add_addons_argument(parser)
parser.add_argument('--module', metavar='MODULE', action='append',
help='specify a module to install'
' (this option can be repeated)')
parser.add_argument('--all-modules', action='store_true',
help='install all visible modules (not compatible with --module)')
parser.add_argument('--no-create', action='store_true',
help='do not create the database, only initialize it')
parser.add_argument('--exclude', metavar='MODULE', action='append',
help='exclude a module from installation'
' (this option can be repeated)')
parser.add_argument('--tests', action='store_true',
help='run the tests as modules are installed'
' (use the `run-tests` command to choose specific'
' tests to run against an existing database).'
' Demo data are installed.')
parser.set_defaults(run=run)

7
openerpcommand/main.py Normal file
View File

@ -0,0 +1,7 @@
import openerpcommand
def run():
""" Main entry point for the openerp-command tool."""
parser = openerpcommand.main_parser()
args = parser.parse_args()
args.run(args)

61
openerpcommand/model.py Normal file
View File

@ -0,0 +1,61 @@
"""
Display information about a given model.
"""
import os
import sys
import textwrap
def run(args):
assert args.database
assert args.model
import openerp
openerp.tools.config['log_level'] = 100
openerp.netsvc.init_logger()
registry = openerp.modules.registry.RegistryManager.get(
args.database, update_module=False)
model = registry.get(args.model)
longest_k = 1
longest_string = 1
columns = model._columns
if args.field and args.field not in columns:
print "No such field."
sys.exit(1)
if args.field:
columns = { args.field: columns[args.field] }
else:
print "Fields (model `%s`, database `%s`):" % (args.model, args.database)
for k, v in columns.items():
longest_k = len(k) if longest_k < len(k) else longest_k
longest_string = len(v.string) \
if longest_string < len(v.string) else longest_string
for k, v in sorted(columns.items()):
attr = []
if v.required:
attr.append("Required")
if v.readonly:
attr.append("Read-only")
attr = '/'.join(attr)
attr = '(' + attr + ')' if attr else attr
if args.verbose:
print v.string, '-- ' + k + ', ' + v._type, attr
else:
print k.ljust(longest_k + 2), v._type, attr
if args.verbose and v.help:
print textwrap.fill(v.help, initial_indent=' ', subsequent_indent=' ')
def add_parser(subparsers):
parser = subparsers.add_parser('model',
description='Display information about a given model for an existing database.')
parser.add_argument('-d', '--database', metavar='DATABASE', required=True,
help='the database to connect to')
parser.add_argument('-m', '--model', metavar='MODEL', required=True,
help='the model for which information should be displayed')
parser.add_argument('-v', '--verbose', action='store_true',
help='display more information')
parser.add_argument('-f', '--field', metavar='FIELD',
help='display information only for this particular field')
parser.set_defaults(run=run)

68
openerpcommand/module.py Normal file
View File

@ -0,0 +1,68 @@
"""
Show module information for a given database or from the file-system.
"""
import os
import sys
import textwrap
from . import common
# TODO provide a --rpc flag to use XML-RPC (with a specific username) instead
# of server-side library.
def run(args):
assert args.database
import openerp
config = openerp.tools.config
config['log_handler'] = [':CRITICAL']
if args.addons:
args.addons = args.addons.split(':')
else:
args.addons = []
config['addons_path'] = ','.join(args.addons)
openerp.netsvc.init_logger()
if args.filesystem:
module_names = common.get_addons_from_paths(args.addons, [])
print "Modules (addons path %s):" % (', '.join(args.addons),)
for x in sorted(module_names):
print x
else:
registry = openerp.modules.registry.RegistryManager.get(
args.database, update_module=False)
xs = []
ir_module_module = registry.get('ir.module.module')
cr = registry.db.cursor() # TODO context manager
try:
ids = ir_module_module.search(cr, openerp.SUPERUSER_ID, [], {})
xs = ir_module_module.read(cr, openerp.SUPERUSER_ID, ids, [], {})
finally:
cr.close()
if xs:
print "Modules (database `%s`):" % (args.database,)
for x in xs:
if args.short:
print '%3d %s' % (x['id'], x['name'])
else:
print '%3d %s %s' % (x['id'], x['name'], {'installed': '(installed)'}.get(x['state'], ''))
else:
print "No module found (database `%s`)." % (args.database,)
def add_parser(subparsers):
parser = subparsers.add_parser('module',
description='Display modules known from a given database or on file-system.')
parser.add_argument('-d', '--database', metavar='DATABASE',
**common.required_or_default('DATABASE', 'the database to modify'))
common.add_addons_argument(parser)
parser.add_argument('-m', '--module', metavar='MODULE', required=False,
help='the module for which information should be shown')
parser.add_argument('-v', '--verbose', action='store_true',
help='display more information')
parser.add_argument('--short', action='store_true',
help='display less information')
parser.add_argument('-f', '--filesystem', action='store_true',
help='display module in the addons path, not in db')
parser.set_defaults(run=run)

60
openerpcommand/read.py Normal file
View File

@ -0,0 +1,60 @@
"""
Read a record.
"""
import os
import sys
import textwrap
# TODO provide a --rpc flag to use XML-RPC (with a specific username) instead
# of server-side library.
def run(args):
assert args.database
assert args.model
import openerp
config = openerp.tools.config
config['log_handler'] = [':CRITICAL']
openerp.netsvc.init_logger()
registry = openerp.modules.registry.RegistryManager.get(
args.database, update_module=False)
model = registry.get(args.model)
cr = registry.db.cursor() # TODO context manager
field_names = [args.field] if args.field else []
if args.short:
# ignore --field
field_names = ['name']
try:
xs = model.read(cr, 1, args.id, field_names, {})
finally:
cr.close()
if xs:
print "Records (model `%s`, database `%s`):" % (args.model, args.database)
x = xs[0]
if args.short:
print str(x['id']) + '.', x['name']
else:
longest_k = 1
for k, v in x.items():
longest_k = len(k) if longest_k < len(k) else longest_k
for k, v in sorted(x.items()):
print (k + ':').ljust(longest_k + 2), v
else:
print "Record not found."
def add_parser(subparsers):
parser = subparsers.add_parser('read',
description='Display a record.')
parser.add_argument('-d', '--database', metavar='DATABASE', required=True,
help='the database to connect to')
parser.add_argument('-m', '--model', metavar='MODEL', required=True,
help='the model for which a record should be read')
parser.add_argument('-i', '--id', metavar='RECORDID', required=True,
help='the record id')
parser.add_argument('-v', '--verbose', action='store_true',
help='display more information')
parser.add_argument('--short', action='store_true',
help='display less information')
parser.add_argument('-f', '--field', metavar='FIELD',
help='display information only for this particular field')
parser.set_defaults(run=run)

202
openerpcommand/run_tests.py Normal file
View File

@ -0,0 +1,202 @@
"""
Execute the unittest2 tests available in OpenERP addons.
"""
import os
import sys
import types
import common
def get_test_modules(module, submodule, explode):
"""
Return a list of submodules containing tests.
`submodule` can be:
- None
- the name of a submodule
- '__fast_suite__'
- '__sanity_checks__'
"""
# Turn command-line module, submodule into importable names.
if module is None:
pass
elif module == 'openerp':
module = 'openerp.tests'
else:
module = 'openerp.addons.' + module + '.tests'
# Try to import the module
try:
__import__(module)
except Exception, e:
if explode:
print 'Can not `import %s`.' % module
import logging
logging.exception('')
sys.exit(1)
else:
if str(e) == 'No module named tests':
# It seems the module has no `tests` sub-module, no problem.
pass
else:
print 'Can not `import %s`.' % module
return []
# Discover available test sub-modules.
m = sys.modules[module]
submodule_names = sorted([x for x in dir(m) \
if x.startswith('test_') and \
isinstance(getattr(m, x), types.ModuleType)])
submodules = [getattr(m, x) for x in submodule_names]
def show_submodules_and_exit():
if submodule_names:
print 'Available submodules are:'
for x in submodule_names:
print ' ', x
sys.exit(1)
if submodule is None:
# Use auto-discovered sub-modules.
ms = submodules
elif submodule == '__fast_suite__':
# Obtain the explicit test sub-modules list.
ms = getattr(sys.modules[module], 'fast_suite', None)
# `suite` was used before the 6.1 release instead of `fast_suite`.
ms = ms if ms else getattr(sys.modules[module], 'suite', None)
if ms is None:
if explode:
print 'The module `%s` has no defined test suite.' % (module,)
show_submodules_and_exit()
else:
ms = []
elif submodule == '__sanity_checks__':
ms = getattr(sys.modules[module], 'checks', None)
if ms is None:
if explode:
print 'The module `%s` has no defined sanity checks.' % (module,)
show_submodules_and_exit()
else:
ms = []
else:
# Pick the command-line-specified test sub-module.
m = getattr(sys.modules[module], submodule, None)
ms = [m]
if m is None:
if explode:
print 'The module `%s` has no submodule named `%s`.' % \
(module, submodule)
show_submodules_and_exit()
else:
ms = []
return ms
def run(args):
import unittest2
import openerp
config = openerp.tools.config
config['db_name'] = args.database
if args.port:
config['xmlrpc_port'] = int(args.port)
config['admin_passwd'] = 'admin'
config['db_password'] = 'a2aevl8w' # TODO from .openerpserverrc
config['addons_path'] = args.addons.replace(':',',')
if args.addons:
args.addons = args.addons.split(':')
else:
args.addons = []
if args.sanity_checks and args.fast_suite:
print 'Only at most one of `--sanity-checks` and `--fast-suite` ' \
'can be specified.'
sys.exit(1)
import logging
openerp.netsvc.init_alternative_logger()
logging.getLogger('openerp').setLevel(logging.CRITICAL)
# Install the import hook, to import openerp.addons.<module>.
openerp.modules.module.initialize_sys_path()
openerp.modules.loading.open_openerp_namespace()
# Extract module, submodule from the command-line args.
if args.module is None:
module, submodule = None, None
else:
splitted = args.module.split('.')
if len(splitted) == 1:
module, submodule = splitted[0], None
elif len(splitted) == 2:
module, submodule = splitted
else:
print 'The `module` argument must have the form ' \
'`module[.submodule]`.'
sys.exit(1)
# Import the necessary modules and get the corresponding suite.
if module is None:
# TODO
modules = common.get_addons_from_paths(args.addons, []) # TODO openerp.addons.base is not included ?
test_modules = []
for module in ['openerp'] + modules:
if args.fast_suite:
submodule = '__fast_suite__'
if args.sanity_checks:
submodule = '__sanity_checks__'
test_modules.extend(get_test_modules(module,
submodule, explode=False))
else:
if submodule and args.fast_suite:
print "Submodule name `%s` given, ignoring `--fast-suite`." % (submodule,)
if submodule and args.sanity_checks:
print "Submodule name `%s` given, ignoring `--sanity-checks`." % (submodule,)
if not submodule and args.fast_suite:
submodule = '__fast_suite__'
if not submodule and args.sanity_checks:
submodule = '__sanity_checks__'
test_modules = get_test_modules(module,
submodule, explode=True)
# Run the test suite.
if not args.dry_run:
suite = unittest2.TestSuite()
for test_module in test_modules:
suite.addTests(unittest2.TestLoader().loadTestsFromModule(test_module))
r = unittest2.TextTestRunner(verbosity=2).run(suite)
if r.errors or r.failures:
sys.exit(1)
else:
print 'Test modules:'
for test_module in test_modules:
print ' ', test_module.__name__
def add_parser(subparsers):
parser = subparsers.add_parser('run-tests',
description='Run the OpenERP server and/or addons tests.')
parser.add_argument('-d', '--database', metavar='DATABASE', required=True,
help='the database to test. Depending on the test suites, the '
'database must already exist or not.')
parser.add_argument('-p', '--port', metavar='PORT',
help='the port used for WML-RPC tests')
common.add_addons_argument(parser)
parser.add_argument('-m', '--module', metavar='MODULE',
default=None,
help='the module to test in `module[.submodule]` notation. '
'Use `openerp` for the core OpenERP tests. '
'Leave empty to run every declared tests. '
'Give a module but no submodule to run all the module\'s declared '
'tests. If both the module and the submodule are given, '
'the sub-module can be run even if it is not declared in the module.')
parser.add_argument('--fast-suite', action='store_true',
help='run only the tests explicitely declared in the fast suite (this '
'makes sense only with the bare `module` notation or no module at '
'all).')
parser.add_argument('--sanity-checks', action='store_true',
help='run only the sanity check tests')
parser.add_argument('--dry-run', action='store_true',
help='do not run the tests')
parser.set_defaults(run=run)

View File

@ -0,0 +1,77 @@
"""
Generate an OpenERP module skeleton.
"""
import os
import sys
def run(args):
assert args.module
module = args.module
if os.path.exists(module):
print "The path `%s` already exists."
sys.exit(1)
os.mkdir(module)
os.mkdir(os.path.join(module, 'models'))
with open(os.path.join(module, '__openerp__.py'), 'w') as h:
h.write(MANIFEST)
with open(os.path.join(module, '__init__.py'), 'w') as h:
h.write(INIT_PY)
with open(os.path.join(module, 'models', '__init__.py'), 'w') as h:
h.write(MODELS_PY % (module,))
def add_parser(subparsers):
parser = subparsers.add_parser('scaffold',
description='Generate an OpenERP module skeleton.')
parser.add_argument('module', metavar='MODULE',
help='the name of the generated module')
parser.set_defaults(run=run)
MANIFEST = """\
# -*- coding: utf-8 -*-
{
'name': '<Module name>',
'version': '0.0',
'category': '<Category>',
'description': '''
<Long description>
''',
'author': '<author>',
'maintainer': '<maintainer>',
'website': 'http://<website>',
# Add any module that are necessary for this module to correctly work in
# the `depends` list.
'depends': ['base'],
'data': [
],
'test': [
],
# Set to False if you want to prevent the module to be known by OpenERP
# (and thus appearing in the list of modules).
'installable': True,
# Set to True if you want the module to be automatically whenever all its
# dependencies are installed.
'auto_install': False,
}
"""
INIT_PY = """\
# -*- coding: utf-8 -*-
import models
"""
MODELS_PY = """\
# -*- coding: utf-8 -*-
import openerp
# Define a new model.
class my_model(openerp.osv.osv.Model):
_name = '%s.my_model'
_columns = {
}
"""

View File

@ -0,0 +1,67 @@
"""
Install OpenERP on a new (by default) database.
"""
import os
import sys
import common
# TODO turn template1 in a parameter
# This should be exposed from openerp (currently in
# openerp/service/web_services.py).
def create_database(database_name):
import openerp
db = openerp.sql_db.db_connect('template1')
cr = db.cursor() # TODO `with db as cr:`
try:
cr.autocommit(True)
cr.execute("""CREATE DATABASE "%s"
ENCODING 'unicode' TEMPLATE "template1" """ \
% (database_name,))
finally:
cr.close()
def run(args):
assert args.database
assert args.module
import openerp
config = openerp.tools.config
config['log_handler'] = [':CRITICAL']
if args.addons:
args.addons = args.addons.split(':')
else:
args.addons = []
config['addons_path'] = ','.join(args.addons)
openerp.netsvc.init_logger()
# Install the import hook, to import openerp.addons.<module>.
openerp.modules.module.initialize_sys_path()
openerp.modules.loading.open_openerp_namespace()
registry = openerp.modules.registry.RegistryManager.get(
args.database, update_module=False)
ir_module_module = registry.get('ir.module.module')
cr = registry.db.cursor() # TODO context manager
try:
ids = ir_module_module.search(cr, openerp.SUPERUSER_ID, [('name', 'in', args.module), ('state', '=', 'installed')], {})
if len(ids) == len(args.module):
ir_module_module.button_immediate_uninstall(cr, openerp.SUPERUSER_ID, ids, {})
else:
print "At least one module not found (database `%s`)." % (args.database,)
finally:
cr.close()
def add_parser(subparsers):
parser = subparsers.add_parser('uninstall',
description='Uninstall some modules from an OpenERP database.')
parser.add_argument('-d', '--database', metavar='DATABASE',
**common.required_or_default('DATABASE', 'the database to modify'))
common.add_addons_argument(parser)
parser.add_argument('--module', metavar='MODULE', action='append',
help='specify a module to uninstall'
' (this option can be repeated)')
parser.set_defaults(run=run)

19
openerpcommand/update.py Normal file
View File

@ -0,0 +1,19 @@
"""
Update an existing OpenERP database.
"""
def run(args):
assert args.database
import openerp
config = openerp.tools.config
config['update']['all'] = 1
openerp.modules.registry.RegistryManager.get(
args.database, update_module=True)
def add_parser(subparsers):
parser = subparsers.add_parser('update',
description='Update an existing OpenERP database.')
parser.add_argument('-d', '--database', metavar='DATABASE', required=True,
help='the database to update')
parser.set_defaults(run=run)