diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..a3d5032 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[run] +omit = + openerp_proxy/tests/*.* + openerp_proxy/main.py diff --git a/.travis.yml b/.travis.yml index 9c509cd..afad36a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,23 @@ language: python python: - "2.7" + - "3.3" + - "3.4" +# - "3.5" env: - ODOO_VERSION="8.0" ODOO_PACKAGE="odoo" ODOO_TEST_PROTOCOL='xml-rpc' - ODOO_VERSION="8.0" ODOO_PACKAGE="odoo" ODOO_TEST_PROTOCOL='json-rpc' - ODOO_VERSION="7.0" ODOO_PACKAGE="openerp" ODOO_TEST_PROTOCOL='xml-rpc' + - ODOO_VERSION="8.0" ODOO_PACKAGE="odoo" ODOO_TEST_PROTOCOL='xml-rpc' TEST_WITH_EXTENSIONS='openerp_proxy.ext.all' + - ODOO_VERSION="8.0" ODOO_PACKAGE="odoo" ODOO_TEST_PROTOCOL='json-rpc' TEST_WITH_EXTENSIONS='openerp_proxy.ext.all' + - ODOO_VERSION="7.0" ODOO_PACKAGE="openerp" ODOO_TEST_PROTOCOL='xml-rpc' TEST_WITH_EXTENSIONS='openerp_proxy.ext.all' install: - "wget http://nightly.odoo.com/${ODOO_VERSION}/nightly/deb/${ODOO_PACKAGE}_${ODOO_VERSION}.latest_all.deb" - "sudo dpkg -i ${ODOO_PACKAGE}_${ODOO_VERSION}.latest_all.deb || true" - "sudo apt-get update && sudo apt-get install -f -y" - - "pip install coveralls" + - "pip install coveralls mock simple-crypt ipython[notebook]" - "python setup.py develop" before_script: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bc80aba..e146ff0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,4 +1,18 @@ master: + - *Backward incompatible*: Changed session file format. + *Start up imports* and *extra_paths* moved to *options* section of file. + - *Backward incompatible*: ``IPYSession`` moved to ``openerp_proxy.ext.repr`` extensions. + Now when using IPython notebook, this extension have to be imported first, + to enable HTML representation of session object + - *Backward incompatible*: Changed signature of ``Session.connect()`` method. + - *Backward incompatible*: Renamed ``ERP_Proxy`` to ``Client`` and inherited objects renamed in such way + (for example sugar extension module) + - *Backward incompatible*: removed ``ERP_Proxy` and ``ERP_Session`` compatability aliases + + - Changed ``store_passwords`` option meaning. now if set it will store passwords bese64 encoded, + instead of using simple-crypt module. This change makes it faster to decode password, + because last-versions of simple-crypt become too slow. and usualy no encryption needed here. + - Experimental *Python 3.3+* support - Added ``HField.with_args`` method. - Added basic implementation of graph plugin. - Improved ``openerp_proxy.ext.log_execute_console`` extension. Added timing. @@ -6,12 +20,9 @@ master: - RecordList prefetching logic moved to cache module and highly refactored (Added support of prefetching of related fields) - Added ``Client.login(dbname, user, password)`` method. - - Changed signature of ``Session.connect()`` method. - Added ``HTMLTable.update`` method. - Added ``RecordList.copy()`` and ``RecordList.existing()`` methods. - Added ``HTMLTable.to_csv()`` method. - - Renamed ``ERP_Proxy`` to ``Client`` and inherited objects renamed in such way - (for example sugar extension module) - Added ``Client.server_version`` property - Client parametrs (dbname, user, pwd) now are not required. This is useful when working with ``db`` service (``client.services.db``) diff --git a/README.rst b/README.rst index 85e36b2..23d626d 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ Overview This project is just **RPC client** for Odoo. It aims to ease access to openerp data via shell and used mostly for data debuging purposes. This project provides interface similar to -OpenERP internal code to perform operations on **OpenERP** / **Odoo** objects hiding +Odoo internal code to perform operations on **OpenERP** / **Odoo** objects hiding **XML-RPC** or **JSON-RPC** behind. @@ -28,67 +28,54 @@ OpenERP internal code to perform operations on **OpenERP** / **Odoo** objects hi Features ~~~~~~~~ -- supports call to all public methods on any OpenERP/Odoo object including: +- *Python 3.3+* support +- You can call any public method on any OpenERP / Odoo object including: *read*, *search*, *write*, *unlink* and others -- Have *a lot of speed optimizations* (especialy for situation, where required processing of - large datasets) +- Have *a lot of speed optimizations* (caching, read only fields accessed, + read data for all records in current set, by one RPC call, etc) - Desinged to take as more benefits of **IPython autocomplete** as posible - Works nice in **IPython Notebook** providing **HTML representation** for a most of objects. -- Ability to display set of records as **HTML Table** - including conditional **row highlighting**. - (Useful in IPython Notebook for *data-analysis*) -- Ability to represent HTML table also as *CSV file* -- Provides session/history functionality, so if You used it to connect to - some database before, new connection will be simpler (just enter password). - Version 0.5 and higher have ability to store passwords. just use - ``session.option('store_passwords', True); session.save()`` +- Ability to export HTML table recordlist representation to *CSV file* +- Ability to save connections to different databases in session. + (By default password is not saved, and will be asked, but if You need to save it, just do this: + ``session.option('store_passwords', True); session.save()``) - Provides *browse\_record* like interface, allowing to browse related - models too. Supports *browse* method. Adds method *search\_records* to simplify + models too. Supports *browse* method. Also adds method *search\_records* to simplify search-and-read operations. - *Extension support*. You can easily modify most of components of this app/lib - creating Your own extensions. It is realy simple. See for examples in + creating Your own extensions and plugins. It is realy simple. See for examples in openerp_proxy/ext/ directory. -- *Plugin Support*. Plugins here meant utils, which could store some aditional - logic, to simplify routine operations. - Accessible from ``db.plugins.`` attribute. -- Support of **JSON-RPC** for *version 8* of OpenERP/Odoo (***experimental***) +- *Plugin Support*. Plugins are same as extensions, but aimed to implement additional logic. + For example look at *openerp_proxy/plugins* and *openerp_proxy/plugin.py* +- Support of **JSON-RPC** for *version 8+* of Odoo - Support of using **named parametrs** in RPC method calls (server version 6.1 and higher). - *Sugar extension* which simplifys code a lot. - Missed feature? ask in `Project Issues `_ -Constraints -~~~~~~~~~~~ +Supported Python versions +~~~~~~~~~~~~~~~~~~~~~~~~~ -For High level functionality Odoo server must be of version 6.1 or higher +Support Python 2.7, 3.3, 3.4 -Examples -~~~~~~~~ +Supported Odoo server versions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- `Examples & HTML tests `_ +Tested with Odoo 7.0 and 8.0 +Also shoud work with Odoo 6.1 and 9.0 -What You can do with this -~~~~~~~~~~~~~~~~~~~~~~~~~ +Also it should work with Odoo version 6.0, except the things related to passing named parametrs +to server methods, such as using context in ``openerp_proxy.orm`` package -- Quickly read and analyze some data that is not visible in interface - without access to DB -- Use this project as library for code that need to access OpenERP data -- Use in scripts that migrates OpenERP data (after, for example, adding - new functionality or changing old). (Migration using only SQL is bad - idea because of functional fields with *store=True* which must be - recalculated). -Near future plans -~~~~~~~~~~~~~~~~~ +Examples +~~~~~~~~ -- Django-like search API implemented as extension - - Something like ``F`` or ``Q`` expressions from Django - - to make working constructions like: - ``object.filter((F('price') > 100.0) & (F('price') != F('Price2')))`` +- `Examples & HTML tests `_ Install @@ -107,7 +94,7 @@ If You want to install development version of *OpenERP Proxy* you can do it via: Also if You plan to use this project as shell client, it is **recommended to install IPython** -and If You would like to have ability to play with Odoo / OpenERP data in IPython notebook, +and If You would like to have ability to play with Odoo data in IPython notebook, it is recommended to also install IPython's Notebook support. To install IPython and IPython Notebook just type:: @@ -127,14 +114,14 @@ And You will get the openerp_proxy shell. If *IPython* is installed then IPython will be used, else usual python shell will be used. There is in context exists *session* variable that represents current session to work with -Next You have to get connection to some OpenERP/Odoo database. +Next You have to get connection to some Odoo database. :: >>> db = session.connect() This will ask You for host, port, database, etc to connect to. Now You -have connection to OpenERP database which allows You to use database +have connection to Odoo database which allows You to use database objects. @@ -152,11 +139,11 @@ So here is a way to create connection :: - import openerp_proxy.core as oe_core - db = oe_core.Client(dbname='my_db', - host='my_host.int', - user='my_db_user', - pwd='my_password here') + from openerp_proxy.core import Client + db = Client(host='my_host.int', + dbname='my_db', + user='my_db_user', + pwd='my_password here') And next all there same, no more differences betwen shell and lib usage. @@ -168,13 +155,13 @@ To better suit for HTML capable notebook You would like to use IPython's version object and *openerp_proxy.ext.repr* extension. So in first cell of notebook import session and extensions/plugins You want:: - from openerp_proxy.session import IPYSession as Session # Use IPython-itegrated session class - import openerp_proxy.ext.repr # Enable representation extension. This provides HTML representation of objects - from openerp_proxy.ext.repr import HField # Used in .as_html_table method of RecordList - # also You may import all standard extensions in one line: from openerp_proxy.ext.all import * + # note that extensions were imported before session, + # because some of them modify Session class + from openerp_proxy.session import Session + session = Session() Now most things same as for shell usage, but... @@ -186,9 +173,10 @@ To solve this, it is recommended to uses *store_passwords* option:: session.option('store_passwords', True) session.save() -In this way, only when You connect first time, You need to explicitly pass password to *connect* of *get_db* methods. +Next use it likt shell (or like lib), but *do not forget to save session, after new connection* -(*do not forget to save session, after new connection*) +*Note*: in old version of IPython getpass was not work correctly, +so maybe You will need to pass password directly to *session.connect* method. General usage @@ -207,9 +195,9 @@ database: So we have 5 orders in done state. So let's read them. -Default way to read data from OpenERP is to search for required records +Default way to read data from Odoo is to search for required records with *search* method which return's list of IDs of records, then read -data using *read* method. Both methods mostly same as OpenERP internal +data using *read* method. Both methods mostly same as Odoo internal ones: :: @@ -348,7 +336,7 @@ So let's start ``vim attendance.py`` -3. write folowing code there (note that this example works and tested for Odoo version 6.0) +3. write folowing code there (note that this example works and tested for Odoo version 6.0 only) :: diff --git a/bin/openerp_proxy b/bin/openerp_proxy index e8a8a5c..1573ca5 100755 --- a/bin/openerp_proxy +++ b/bin/openerp_proxy @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf8 -*- -from openerp_proxy import main +from openerp_proxy.main import main if __name__ == '__main__': main() diff --git a/docs/source/conf.py b/docs/source/conf.py index 54d2d1f..634881b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -51,7 +51,7 @@ # General information about the project. project = u'openerp_proxy' -copyright = u'2014, Dmytro Katyukha' +copyright = u'2015, Dmytro Katyukha' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -105,7 +105,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/source/intro.rst b/docs/source/intro.rst index 1b9496e..23d626d 100644 --- a/docs/source/intro.rst +++ b/docs/source/intro.rst @@ -1,70 +1,81 @@ OpenERP / Odoo proxy ==================== -This project aims to ease access to openerp data via shell and used -mostly for debug purposes. This project provides interface similar to -OpenERP internal code to perform operations on **OpenERP** / **Odoo** object hiding -XML-RPC behind +Build Status +------------ + +.. image:: https://travis-ci.org/katyukha/openerp-proxy.svg?branch=master + :target: https://travis-ci.org/katyukha/openerp-proxy + +.. image:: https://coveralls.io/repos/katyukha/openerp-proxy/badge.svg?branch=master&service=github + :target: https://coveralls.io/github/katyukha/openerp-proxy?branch=master + Overview -------- +This project is just **RPC client** for Odoo. +It aims to ease access to openerp data via shell and used +mostly for data debuging purposes. This project provides interface similar to +Odoo internal code to perform operations on **OpenERP** / **Odoo** objects hiding +**XML-RPC** or **JSON-RPC** behind. + + + - Are You still using pgAdmin for quering Odoo database? + - Try this package (expecialy via IPython Notebook), and You will forget about pgAdmin! + + Features ~~~~~~~~ -- supports call to all public methods on any OpenERP/Odoo object including: +- *Python 3.3+* support +- You can call any public method on any OpenERP / Odoo object including: *read*, *search*, *write*, *unlink* and others -- Designed not for speed but to be useful like cli client to OpenERP/Odoo - (*Versiion 0.5 introduces orm optimizations*) +- Have *a lot of speed optimizations* (caching, read only fields accessed, + read data for all records in current set, by one RPC call, etc) - Desinged to take as more benefits of **IPython autocomplete** as posible -- Also it works good enough in **IPython Notebook** providing **HTML - representation** for a lot of objects. -- Ability to display set of records as **HTML Table** - including **row highlighting** -- Provides session/history functionality, so if You used it to connect to - some database before, new connection will be simpler (just enter password). - Version 0.5 and higher have ability to store passwords. just use - ``session.option('store_passwords', True); session.save()`` +- Works nice in **IPython Notebook** providing **HTML + representation** for a most of objects. +- Ability to export HTML table recordlist representation to *CSV file* +- Ability to save connections to different databases in session. + (By default password is not saved, and will be asked, but if You need to save it, just do this: + ``session.option('store_passwords', True); session.save()``) - Provides *browse\_record* like interface, allowing to browse related - models too. But use's methods *search\_records* and *browse\_records* - instead of *browse*. (From version 0.4 *browse* works too) + models too. Supports *browse* method. Also adds method *search\_records* to simplify + search-and-read operations. - *Extension support*. You can easily modify most of components of this app/lib - creating Your own extensions. It is realy simple. See for examples in + creating Your own extensions and plugins. It is realy simple. See for examples in openerp_proxy/ext/ directory. -- *Plugin Support*. Plugins here meant utils, which could store some aditional - logic, to simplify routine operations. - Accessible from ``db.plugins.`` attribute. -- Support of **JSON-RPC** for *version 8* of OpenERP/Odoo (*experimental*) +- *Plugin Support*. Plugins are same as extensions, but aimed to implement additional logic. + For example look at *openerp_proxy/plugins* and *openerp_proxy/plugin.py* +- Support of **JSON-RPC** for *version 8+* of Odoo - Support of using **named parametrs** in RPC method calls (server version 6.1 and higher). +- *Sugar extension* which simplifys code a lot. - Missed feature? ask in `Project Issues `_ -Examples -~~~~~~~~ -- `Examples & HTML tests `_ +Supported Python versions +~~~~~~~~~~~~~~~~~~~~~~~~~ +Support Python 2.7, 3.3, 3.4 -What You can do with this -~~~~~~~~~~~~~~~~~~~~~~~~~ -- Quickly read and analyze some data that is not visible in interface - without access to DB -- Use this project as library for code that need to access OpenERP data -- Use in scripts that migrates OpenERP data (after, for example, adding - new functionality or changing old). (Migration using only SQL is bad - idea because of functional fields with *store=True* which must be - recalculated). +Supported Odoo server versions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tested with Odoo 7.0 and 8.0 + +Also shoud work with Odoo 6.1 and 9.0 + +Also it should work with Odoo version 6.0, except the things related to passing named parametrs +to server methods, such as using context in ``openerp_proxy.orm`` package -Near future plans -~~~~~~~~~~~~~~~~~ -- Better plugin system which will allow to extend API on database, - object, and record levels. **DONE** -- Django-like search API implemented as extension - - Something like ``F`` or ``Q`` expressions from Django - - to make working constructions like: - ``object.filter((F('price') > 100.0) & (F('price') != F('Price2')))`` +Examples +~~~~~~~~ + +- `Examples & HTML tests `_ Install @@ -82,8 +93,8 @@ If You want to install development version of *OpenERP Proxy* you can do it via: pip install -e git+https://github.com/katyukha/openerp-proxy.git#egg=openerp_proxy -Also if You plan to use this project as shell client, it is recommended to install IPython -and If You would like to have ability to play with Odoo / OpenERP data in IPython notebook, +Also if You plan to use this project as shell client, it is **recommended to install IPython** +and If You would like to have ability to play with Odoo data in IPython notebook, it is recommended to also install IPython's Notebook support. To install IPython and IPython Notebook just type:: @@ -103,14 +114,14 @@ And You will get the openerp_proxy shell. If *IPython* is installed then IPython will be used, else usual python shell will be used. There is in context exists *session* variable that represents current session to work with -Next You have to get connection to some OpenERP/Odoo database. +Next You have to get connection to some Odoo database. :: >>> db = session.connect() This will ask You for host, port, database, etc to connect to. Now You -have connection to OpenERP database which allows You to use database +have connection to Odoo database which allows You to use database objects. @@ -128,11 +139,11 @@ So here is a way to create connection :: - import openerp_proxy.core as oe_core - db = oe_core.ERP_Proxy(dbname='my_db', - host='my_host.int', - user='my_db_user', - pwd='my_password here') + from openerp_proxy.core import Client + db = Client(host='my_host.int', + dbname='my_db', + user='my_db_user', + pwd='my_password here') And next all there same, no more differences betwen shell and lib usage. @@ -144,9 +155,12 @@ To better suit for HTML capable notebook You would like to use IPython's version object and *openerp_proxy.ext.repr* extension. So in first cell of notebook import session and extensions/plugins You want:: - from openerp_proxy.session import IPYSession as Session # Use IPython-itegrated session class - import openerp_proxy.ext.repr # Enable representation extension. This provides HTML representation of objects - from openerp_proxy.ext.repr import HField # Used in .as_html_table method of RecordList + # also You may import all standard extensions in one line: + from openerp_proxy.ext.all import * + + # note that extensions were imported before session, + # because some of them modify Session class + from openerp_proxy.session import Session session = Session() @@ -159,9 +173,10 @@ To solve this, it is recommended to uses *store_passwords* option:: session.option('store_passwords', True) session.save() -In this way, only when You connect first time, You need to explicitly pass password to *connect* of *get_db* methods. +Next use it likt shell (or like lib), but *do not forget to save session, after new connection* -(*do not forget to save session, after new connection*) +*Note*: in old version of IPython getpass was not work correctly, +so maybe You will need to pass password directly to *session.connect* method. General usage @@ -180,9 +195,9 @@ database: So we have 5 orders in done state. So let's read them. -Default way to read data from OpenERP is to search for required records +Default way to read data from Odoo is to search for required records with *search* method which return's list of IDs of records, then read -data using *read* method. Both methods mostly same as OpenERP internal +data using *read* method. Both methods mostly same as Odoo internal ones: :: @@ -302,12 +317,13 @@ Plugins ------- In version 0.4 plugin system was completly refactored. At this version -we start using *extend_me* library to build extensions and plugins. +we start using [*extend_me*](https://pypi.python.org/pypi/extend_me) +library to build extensions and plugins easily. Plugins are usual classes that provides functionality that should be available at ``db.plugins.*`` point, implementing logic not related to core system. -To ilustrate what is plugins and what they can do we will create one. +To ilustrate what is plugins and what they can do we will create a simplest one. So let's start 1. create some directory to place plugins in: @@ -320,7 +336,7 @@ So let's start ``vim attendance.py`` -3. write folowing code there +3. write folowing code there (note that this example works and tested for Odoo version 6.0 only) :: diff --git a/docs/source/module_ref/openerp_proxy.ext.rst b/docs/source/module_ref/openerp_proxy.ext.rst index 8fbb61e..99643ec 100644 --- a/docs/source/module_ref/openerp_proxy.ext.rst +++ b/docs/source/module_ref/openerp_proxy.ext.rst @@ -8,14 +8,6 @@ :undoc-members: :show-inheritance: -:mod:`data` Module ------------------- - -.. automodule:: openerp_proxy.ext.data - :members: - :undoc-members: - :show-inheritance: - :mod:`sugar` Module ------------------- diff --git a/docs/source/module_ref/openerp_proxy.orm.rst b/docs/source/module_ref/openerp_proxy.orm.rst index 3411025..bd5db93 100644 --- a/docs/source/module_ref/openerp_proxy.orm.rst +++ b/docs/source/module_ref/openerp_proxy.orm.rst @@ -16,6 +16,14 @@ :undoc-members: :show-inheritance: +:mod:`cache` Module +-------------------- + +.. automodule:: openerp_proxy.orm.cache + :members: + :undoc-members: + :show-inheritance: + :mod:`record` Module -------------------- diff --git a/docs/source/module_ref/openerp_proxy.service.rst b/docs/source/module_ref/openerp_proxy.service.rst index 2df79a4..e5da3a1 100644 --- a/docs/source/module_ref/openerp_proxy.service.rst +++ b/docs/source/module_ref/openerp_proxy.service.rst @@ -8,6 +8,14 @@ :undoc-members: :show-inheritance: +:mod:`db` Module +-------------------- + +.. automodule:: openerp_proxy.service.db + :members: + :undoc-members: + :show-inheritance: + :mod:`object` Module -------------------- diff --git a/examples/Examples & HTML tests.ipynb b/examples/Examples & HTML tests.ipynb index 903b5a9..6b5a4e6 100644 --- a/examples/Examples & HTML tests.ipynb +++ b/examples/Examples & HTML tests.ipynb @@ -24,10 +24,10 @@ { "data": { "text/html": [ - "
xml-rpc://admin@localhost:8069/demo_db_1
Hostlocalhost
Port8069
Protocolxml-rpc
Databasedemo_db_1
loginadmin
To get list of registered objects for thist database
access registered_objects property:
 .registered_objectsTo get Object instance just call get_obj method
 .get_obj(name)
where name is name of Object You want to get
or use get item syntax instead:
 [name]
" + "
xml-rpc://admin@localhost:8069/openerp_proxy_test_db
Hostlocalhost
Port8069
Protocolxml-rpc
Databaseopenerp_proxy_test_db
loginadmin
To get list of registered objects for thist database
access registered_objects property:
 .registered_objectsTo get Object instance just call get_obj method
 .get_obj(name)
where name is name of Object You want to get
or use get item syntax instead:
 [name]
" ], "text/plain": [ - "Client: xml-rpc://admin@localhost:8069/demo_db_1" + "Client: xml-rpc://admin@localhost:8069/openerp_proxy_test_db" ] }, "execution_count": 1, @@ -36,19 +36,23 @@ } ], "source": [ - "from openerp_proxy.session import IPYSession as Session\n", + "from openerp_proxy.ext.all import HField # import extensions first (they modify Session and Client classes)\n", + "\n", "from openerp_proxy.core import Client\n", - "from openerp_proxy.ext.all import HField\n", "import openerp_proxy.plugins.module_utils # Enable module_utils plugin\n", "\n", + "from openerp_proxy.session import Session\n", + "\n", "# connect to local instance of server\n", "cl = Client('localhost')\n", "\n", - "# create demo database\n", - "cl.services.db.create_db('admin', 'demo_db_1', demo=True, lang='en_US')\n", + "# check if our demo database exists\n", + "if 'openerp_proxy_test_db' not in cl.services.db.list_db():\n", + " # create demo database\n", + " cl.services.db.create_db('admin', 'openerp_proxy_test_db', demo=True, lang='en_US')\n", "\n", "# login to created database\n", - "ldb = cl.login('demo_db_1', 'admin', 'admin') # all this arguments could be passed directly to Client constructor.\n", + "ldb = cl.login('openerp_proxy_test_db', 'admin', 'admin') # all this arguments could be passed directly to Client constructor.\n", "\n", "# Note that both 'cl' and 'ldb' are instances of same class\n", "# the difference is in presense of database connection args.\n", @@ -76,7 +80,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 2, "metadata": { "collapsed": false }, @@ -95,7 +99,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 3, "metadata": { "collapsed": false }, @@ -106,7 +110,7 @@ "True" ] }, - "execution_count": 12, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -124,7 +128,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 4, "metadata": { "collapsed": false }, @@ -132,13 +136,13 @@ { "data": { "text/html": [ - "
Previous connections
DB URLDB IndexDB Aliases
xml-rpc://admin@localhost:8069/demo_db_11
To get connection just call
  • session.aliase
  • session[index]
  • session[aliase]
  • session[url]
  • session.get_db(url|index|aliase)
" + "
Previous connections
DB URLDB IndexDB Aliases
xml-rpc://admin@localhost:8069/demo_db_12
xml-rpc://admin@localhost:8069/openerp_proxy_test_db1ldb
To get connection just call
  • session.aliase
  • session[index]
  • session[aliase]
  • session[url]
  • session.get_db(url|index|aliase)
" ], "text/plain": [ - "" + "" ] }, - "execution_count": 13, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -157,7 +161,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 5, "metadata": { "collapsed": false }, @@ -165,13 +169,13 @@ { "data": { "text/html": [ - "
Previous connections
DB URLDB IndexDB Aliases
xml-rpc://admin@localhost:8069/demo_db_11ldb
To get connection just call
  • session.aliase
  • session[index]
  • session[aliase]
  • session[url]
  • session.get_db(url|index|aliase)
" + "
Previous connections
DB URLDB IndexDB Aliases
xml-rpc://admin@localhost:8069/demo_db_12
xml-rpc://admin@localhost:8069/openerp_proxy_test_db1ldb
To get connection just call
  • session.aliase
  • session[index]
  • session[aliase]
  • session[url]
  • session.get_db(url|index|aliase)
" ], "text/plain": [ - "" + "" ] }, - "execution_count": 14, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -192,7 +196,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 6, "metadata": { "collapsed": true }, @@ -217,7 +221,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 7, "metadata": { "collapsed": false }, @@ -225,13 +229,13 @@ { "data": { "text/html": [ - "
xml-rpc://admin@localhost:8069/demo_db_1
Hostlocalhost
Port8069
Protocolxml-rpc
Databasedemo_db_1
loginadmin
To get list of registered objects for thist database
access registered_objects property:
 .registered_objectsTo get Object instance just call get_obj method
 .get_obj(name)
where name is name of Object You want to get
or use get item syntax instead:
 [name]
" + "
xml-rpc://admin@localhost:8069/openerp_proxy_test_db
Hostlocalhost
Port8069
Protocolxml-rpc
Databaseopenerp_proxy_test_db
loginadmin
To get list of registered objects for thist database
access registered_objects property:
 .registered_objectsTo get Object instance just call get_obj method
 .get_obj(name)
where name is name of Object You want to get
or use get item syntax instead:
 [name]
" ], "text/plain": [ - "Client: xml-rpc://admin@localhost:8069/demo_db_1" + "Client: xml-rpc://admin@localhost:8069/openerp_proxy_test_db" ] }, - "execution_count": 2, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -261,7 +265,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 8, "metadata": { "collapsed": false }, @@ -269,34 +273,10 @@ { "data": { "text/plain": [ - "{'auto_refresh': 0,\n", - " 'auto_search': True,\n", - " 'context': {'disable_log': True},\n", - " 'domain': False,\n", - " 'filter': False,\n", - " 'groups_id': [],\n", - " 'help': False,\n", - " 'id': 291,\n", - " 'limit': 80,\n", - " 'multi': False,\n", - " 'name': 'Configure Accounting Data',\n", - " 'nodestroy': False,\n", - " 'res_id': 0,\n", - " 'res_model': 'account.installer',\n", - " 'search_view': '{\\'name\\': \\'default\\', \\'fields\\': {\\'date_stop\\': {\\'selectable\\': True, \\'required\\': True, \\'type\\': \\'date\\', \\'string\\': \\'End Date\\', \\'views\\': {}}}, \\'arch\\': \\'\\', \\'model\\': \\'account.installer\\', \\'type\\': \\'search\\', \\'view_id\\': 0, \\'field_parent\\': False}',\n", - " 'search_view_id': False,\n", - " 'src_model': False,\n", - " 'target': 'new',\n", - " 'type': 'ir.actions.act_window',\n", - " 'usage': False,\n", - " 'view_id': [474, 'account.installer.form'],\n", - " 'view_ids': [],\n", - " 'view_mode': 'form',\n", - " 'view_type': 'form',\n", - " 'views': [[474, 'form']]}" + "{'tag': 'reload', 'type': 'ir.actions.client'}" ] }, - "execution_count": 3, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -314,7 +294,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 9, "metadata": { "collapsed": true }, @@ -341,7 +321,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 10, "metadata": { "collapsed": false }, @@ -349,13 +329,13 @@ { "data": { "text/html": [ - "
Object 'Sales Order'
NameSales Order
Proxyxml-rpc://admin@localhost:8069/demo_db_1
Modelsale.order
Record count8
To get information about columns access property
 .columns_info
Also there are available standard server-side methods:
 search, read, write, unlink
And special methods provided openerp_proxy's orm:
  • search_records - same as search but returns RecordList instance
  • read_records - same as read but returns Record or RecordList instance

" + "
Object 'Sales Order'
NameSales Order
Proxyxml-rpc://admin@localhost:8069/openerp_proxy_test_db
Modelsale.order
Record count8
To get information about columns access property
 .columns_info
Also there are available standard server-side methods:
 search, read, write, unlink
And special methods provided openerp_proxy's orm:
  • search_records - same as search but returns RecordList instance
  • read_records - same as read but returns Record or RecordList instance

" ], "text/plain": [ "Object ('sale.order')" ] }, - "execution_count": 4, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -374,7 +354,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 11, "metadata": { "collapsed": false }, @@ -382,13 +362,13 @@ { "data": { "text/html": [ - "
Object 'Sales Order'
NameSales Order
Proxyxml-rpc://admin@localhost:8069/demo_db_1
Modelsale.order
Record count8
To get information about columns access property
 .columns_info
Also there are available standard server-side methods:
 search, read, write, unlink
And special methods provided openerp_proxy's orm:
  • search_records - same as search but returns RecordList instance
  • read_records - same as read but returns Record or RecordList instance

" + "
Object 'Sales Order'
NameSales Order
Proxyxml-rpc://admin@localhost:8069/openerp_proxy_test_db
Modelsale.order
Record count8
To get information about columns access property
 .columns_info
Also there are available standard server-side methods:
 search, read, write, unlink
And special methods provided openerp_proxy's orm:
  • search_records - same as search but returns RecordList instance
  • read_records - same as read but returns Record or RecordList instance

" ], "text/plain": [ "Object ('sale.order')" ] }, - "execution_count": 5, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -409,7 +389,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 12, "metadata": { "collapsed": false }, @@ -417,13 +397,13 @@ { "data": { "text/html": [ - "
Object 'Sales Order'
NameSales Order
Proxyxml-rpc://admin@localhost:8069/demo_db_1
Modelsale.order
Record count8
To get information about columns access property
 .columns_info
Also there are available standard server-side methods:
 search, read, write, unlink
And special methods provided openerp_proxy's orm:
  • search_records - same as search but returns RecordList instance
  • read_records - same as read but returns Record or RecordList instance

" + "
Object 'Sales Order'
NameSales Order
Proxyxml-rpc://admin@localhost:8069/openerp_proxy_test_db
Modelsale.order
Record count8
To get information about columns access property
 .columns_info
Also there are available standard server-side methods:
 search, read, write, unlink
And special methods provided openerp_proxy's orm:
  • search_records - same as search but returns RecordList instance
  • read_records - same as read but returns Record or RecordList instance

" ], "text/plain": [ "Object ('sale.order')" ] }, - "execution_count": 6, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -442,7 +422,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 13, "metadata": { "collapsed": false, "scrolled": false @@ -471,7 +451,7 @@ " 'help': 'The tax amount.',\n", " 'readonly': 1,\n", " 'selectable': True,\n", - " 'store': \"{'sale.order': ( at 0x73761b8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", + " 'store': \"{'sale.order': ( at 0x78381b8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", " 'string': 'Taxes',\n", " 'type': 'float'},\n", " 'amount_total': {'digits': [16, 2],\n", @@ -482,7 +462,7 @@ " 'help': 'The total amount.',\n", " 'readonly': 1,\n", " 'selectable': True,\n", - " 'store': \"{'sale.order': ( at 0x73762a8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", + " 'store': \"{'sale.order': ( at 0x78382a8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", " 'string': 'Total',\n", " 'type': 'float'},\n", " 'amount_untaxed': {'digits': [16, 2],\n", @@ -493,7 +473,7 @@ " 'help': 'The amount without tax.',\n", " 'readonly': 1,\n", " 'selectable': True,\n", - " 'store': \"{'sale.order': ( at 0x73760c8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", + " 'store': \"{'sale.order': ( at 0x78380c8>, ['order_line'], 10), 'sale.order.line': (, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10)}\",\n", " 'string': 'Untaxed Amount',\n", " 'type': 'float'},\n", " 'client_order_ref': {'selectable': True,\n", @@ -791,7 +771,7 @@ " 'type': 'many2one'}}" ] }, - "execution_count": 7, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -809,7 +789,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 14, "metadata": { "collapsed": false, "scrolled": true @@ -818,13 +798,13 @@ { "data": { "text/html": [ - "
RecordList(sale.order): length=8
ObjectObject ('sale.order')
Proxyxml-rpc://admin@localhost:8069/demo_db_1
Record count8
To get table representation of data call method
 .as_html_table
passing as arguments fields You want to see in resulting table
for better information get doc on as_html_table method:
 .as_html_table?
example of using this mehtod:
 .as_html_table('id','name','_name')
Here _name field is aliase for result of name_get methodcalled on record
" + "
RecordList(sale.order): length=8
ObjectObject ('sale.order')
Proxyxml-rpc://admin@localhost:8069/openerp_proxy_test_db
Record count8
To get table representation of data call method
 .as_html_table
passing as arguments fields You want to see in resulting table
for better information get doc on as_html_table method:
 .as_html_table?
example of using this mehtod:
 .as_html_table('id','name','_name')
Here _name field is aliase for result of name_get methodcalled on record
" ], "text/plain": [ "RecordList(sale.order): length=8" ] }, - "execution_count": 8, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -844,7 +824,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 15, "metadata": { "collapsed": false }, @@ -852,13 +832,13 @@ { "data": { "text/html": [ - "
RecordList(sale.order): length=8
ObjectObject ('sale.order')
Proxyxml-rpc://admin@localhost:8069/demo_db_1
Record count8
To get table representation of data call method
 .as_html_table
passing as arguments fields You want to see in resulting table
for better information get doc on as_html_table method:
 .as_html_table?
example of using this mehtod:
 .as_html_table('id','name','_name')
Here _name field is aliase for result of name_get methodcalled on record
" + "
RecordList(sale.order): length=8
ObjectObject ('sale.order')
Proxyxml-rpc://admin@localhost:8069/openerp_proxy_test_db
Record count8
To get table representation of data call method
 .as_html_table
passing as arguments fields You want to see in resulting table
for better information get doc on as_html_table method:
 .as_html_table?
example of using this mehtod:
 .as_html_table('id','name','_name')
Here _name field is aliase for result of name_get methodcalled on record
" ], "text/plain": [ "RecordList(sale.order): length=8" ] }, - "execution_count": 9, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -885,7 +865,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 16, "metadata": { "collapsed": false }, @@ -893,13 +873,13 @@ { "data": { "text/html": [ - "
Note, that You may use .to_csv() method of this table to export it to CSV format
RecordList(sale.order): length=8
idnamePartner namePartner emailorder_line.as_html_listRelated Invoicesstate
8SO008Millennium IndustriesFalse
  • 20: Laptop Customized
  • 21: Mouse, Wireless
    draft
    7SO007Luminous TechnologiesFalse
    • 16: Laptop E5023
    • 17: GrapWorks Software
    • 18: Datacard
    • 19: USB Adapter
      manual
      6SO006Think Big Systemsinfo@thinkbig.com
      • 15: PC Assamble + 2GB RAM
        draft
        5SO005Agrolaitinfo@agrolait.com
        • 12: External Hard disk
        • 13: Blank DVD-RW
        • 14: Printer, All-in-one
          draft
          4SO004Millennium IndustriesFalse
          • 8: Service on demand
          • 9: Webcam
          • 10: Multimedia Speakers
          • 11: Switch, 24 ports
            draft
            3SO003Chamber Worksinfo@chamberworks.com
            • 6: On Site Monitoring
            • 7: Toner Cartridge
              draft
              2SO002Bank Wealthy and sonsemail@wealthyandsons.com
              • 4: Service on demand
              • 5: On Site Assistance
                draft
                1SO001Agrolaitinfo@agrolait.com
                • 1: Laptop E5023
                • 2: Pen drive, 16GB
                • 3: Headset USB
                  draft
                  " + "
                  Note, that You may use .to_csv() method of this table to export it to CSV format
                  RecordList(sale.order): length=8
                  idnamePartner namePartner emailorder_line.as_html_listRelated Invoicesstate
                  8SO008Millennium IndustriesFalse
                  • 20: Laptop Customized
                  • 21: Mouse, Wireless
                    draft
                    7SO007Luminous TechnologiesFalse
                    • 16: Laptop E5023
                    • 17: GrapWorks Software
                    • 18: Datacard
                    • 19: USB Adapter
                      manual
                      6SO006Think Big Systemsinfo@thinkbig.com
                      • 15: PC Assamble + 2GB RAM
                        draft
                        5SO005Agrolaitinfo@agrolait.com
                        • 12: External Hard disk
                        • 13: Blank DVD-RW
                        • 14: Printer, All-in-one
                          draft
                          4SO004Millennium IndustriesFalse
                          • 8: Service on demand
                          • 9: Webcam
                          • 10: Multimedia Speakers
                          • 11: Switch, 24 ports
                            draft
                            3SO003Chamber Worksinfo@chamberworks.com
                            • 6: On Site Monitoring
                            • 7: Toner Cartridge
                              draft
                              2SO002Bank Wealthy and sonsemail@wealthyandsons.com
                              • 4: Service on demand
                              • 5: On Site Assistance
                                draft
                                1SO001Agrolaitinfo@agrolait.com
                                • 1: Laptop E5023
                                • 2: Pen drive, 16GB
                                • 3: Headset USB
                                  done
                                  " ], "text/plain": [ "" ] }, - "execution_count": 10, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -944,7 +924,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 17, "metadata": { "collapsed": false }, @@ -952,13 +932,13 @@ { "data": { "text/html": [ - "./tmp/csv/tmpFI0iVI.csv
                                  " + "./tmp/csv/tmpCnqAk0.csv
                                  " ], "text/plain": [ - "/home/katyukha/projects/erp-proxy/examples/tmp/csv/tmpFI0iVI.csv" + "/home/katyukha/projects/erp-proxy/examples/tmp/csv/tmpCnqAk0.csv" ] }, - "execution_count": 11, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -976,7 +956,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 18, "metadata": { "collapsed": false }, @@ -984,13 +964,13 @@ { "data": { "text/html": [ - "
                                  R(sale.order, 8)[SO008]
                                  ObjectObject ('sale.order')
                                  Proxyxml-rpc://admin@localhost:8069/demo_db_1
                                  NameSO008
                                  To get HTML Table representation of this record call method:
                                   .as_html()
                                  Optionaly You can pass list of fields You want to see:
                                   .as_html('name', 'origin')
                                  for better information get doc on as_html method:
                                   .as_html?
                                  " + "
                                  R(sale.order, 8)[SO008]
                                  ObjectObject ('sale.order')
                                  Proxyxml-rpc://admin@localhost:8069/openerp_proxy_test_db
                                  NameSO008
                                  To get HTML Table representation of this record call method:
                                   .as_html()
                                  Optionaly You can pass list of fields You want to see:
                                   .as_html('name', 'origin')
                                  for better information get doc on as_html method:
                                   .as_html?
                                  " ], "text/plain": [ "R(sale.order, 8)[SO008]" ] }, - "execution_count": 12, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -1008,7 +988,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 19, "metadata": { "collapsed": false }, @@ -1022,7 +1002,7 @@ "" ] }, - "execution_count": 13, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -1037,7 +1017,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 20, "metadata": { "collapsed": false }, @@ -1045,13 +1025,13 @@ { "data": { "text/html": [ - "
                                  Record SO008
                                  ColumnValue
                                  Confirmation DateFalse
                                  Contract / AnalyticFalse
                                  Create Invoicemanual
                                  Creation Date2015-07-14 12:40:59
                                  CustomerR(res.partner, 19)[Millennium Industries]
                                  Customer ReferenceFalse
                                  Date2015-07-14
                                  Delivery AddressR(res.partner, 52)[Millennium Industries, Jacob Taylor]
                                  Fiscal PositionFalse
                                  Invoice AddressR(res.partner, 52)[Millennium Industries, Jacob Taylor]
                                  Invoice onorder
                                  InvoicesRecordList(account.invoice): length=0
                                  MessagesRecordList(mail.message): length=2
                                  Order LinesRecordList(sale.order.line): length=2
                                  Order ReferenceSO008
                                  Payment TermFalse
                                  PricelistR(product.pricelist, 1)[Public Pricelist (EUR)]
                                  SalespersonR(res.users, 3)[Demo User]
                                  ShopR(sale.shop, 1)[Your Company]
                                  Source DocumentFalse
                                  Statusdraft
                                  Terms and conditionsFalse
                                  " + "
                                  Record SO008
                                  ColumnValue
                                  Confirmation DateFalse
                                  Contract / AnalyticFalse
                                  Create Invoicemanual
                                  Creation Date2015-08-28 13:14:32
                                  CustomerR(res.partner, 19)[Millennium Industries]
                                  Customer ReferenceFalse
                                  Date2015-08-28
                                  Delivery AddressR(res.partner, 52)[Millennium Industries, Jacob Taylor]
                                  Fiscal PositionFalse
                                  Invoice AddressR(res.partner, 52)[Millennium Industries, Jacob Taylor]
                                  Invoice onorder
                                  InvoicesRecordList(account.invoice): length=0
                                  MessagesRecordList(mail.message): length=2
                                  Order LinesRecordList(sale.order.line): length=2
                                  Order ReferenceSO008
                                  Payment TermFalse
                                  PricelistR(product.pricelist, 1)[Public Pricelist (EUR)]
                                  SalespersonR(res.users, 3)[Demo User]
                                  ShopR(sale.shop, 1)[Your Company]
                                  Source DocumentFalse
                                  Statusdraft
                                  Terms and conditionsFalse
                                  " ], "text/plain": [ "" ] }, - "execution_count": 14, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } diff --git a/openerp_proxy/__init__.py b/openerp_proxy/__init__.py index dda144b..e69de29 100644 --- a/openerp_proxy/__init__.py +++ b/openerp_proxy/__init__.py @@ -1,69 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf8 -*- - - -HELP_HEADER = """ - Usage: - >>> db = session.connect() - >>> so_obj = db['sale.orderl'] # get object - >>> dir(so_obj) # Thid will show all default methods of object - >>> so_id = 123 # ID of sale order - >>> so_obj.read(so_id) - >>> so_obj.write([so_id], {'note': 'Test'}) - >>> sm_obj = db['stock.move'] - >>> - >>> # check availability of stock move (call server-side method) - >>> sm_obj.check_assign([move_id1, move_id2,...]) - - Available objects in context: - ERP_Proxy - class that represents single OpenERP database and - provides methods to work with data. Instances of this - class returned by connect() method of session object. - session - represents session of client, stores in home directory list - of databases user works with, to simplify work. It is simpler - to get list of databases you have worked with previously on program - start, and to connect to them without remembrering hosts, users, ports - and other unneccesary information - - Databases You previously worked with: %(databases)s - - Aliases: %(aliases)s - - (Used index or url or aliase for session: session[1] or session[url] or session[aliase]) -""" - - -def main(): - """ Entry point for running as standalone APP - """ - from session import Session - from core import Client - - session = Session() - - header_databases = "\n" - for index, url in session.index.iteritems(): - header_databases += " - [%3s] %s\n" % (index, url) - - header_aliases = "\n" - for aliase, url in session.aliases.iteritems(): - header_aliases += " - %7s: %s\n" % (aliase, url) - - header = HELP_HEADER % {'databases': header_databases, 'aliases': header_aliases} - - _locals = { - 'ERP_Proxy': Client, - 'Client': Client, - 'session': session, - } - try: - from IPython import embed - embed(user_ns=_locals, header=header) - except ImportError: - from code import interact - interact(local=_locals, banner=header) - - session.save() - -if __name__ == '__main__': - main() diff --git a/openerp_proxy/connection/__init__.py b/openerp_proxy/connection/__init__.py index 5e82a8d..590b655 100644 --- a/openerp_proxy/connection/__init__.py +++ b/openerp_proxy/connection/__init__.py @@ -1,3 +1,3 @@ -import xmlrpc -import jsonrpc -from connection import * +import openerp_proxy.connection.xmlrpc +import openerp_proxy.connection.jsonrpc +from .connection import * diff --git a/openerp_proxy/connection/connection.py b/openerp_proxy/connection/connection.py index b80215f..38d275f 100644 --- a/openerp_proxy/connection/connection.py +++ b/openerp_proxy/connection/connection.py @@ -1,3 +1,4 @@ +import six from extend_me import ExtensibleByHashType __all__ = ('get_connector', 'get_connector_names', 'ConnectorBase') @@ -17,10 +18,9 @@ def get_connector_names(): return ConnectorType.get_registered_names() -class ConnectorBase(object): +class ConnectorBase(six.with_metaclass(ConnectorType)): """ Base class for all connectors """ - __metaclass__ = ConnectorType def __init__(self, host, port, verbose=False): self.host = host diff --git a/openerp_proxy/connection/jsonrpc.py b/openerp_proxy/connection/jsonrpc.py index ec40829..064e6d3 100644 --- a/openerp_proxy/connection/jsonrpc.py +++ b/openerp_proxy/connection/jsonrpc.py @@ -1,11 +1,10 @@ # python imports import json -import urllib2 import random +import requests # project imports -from openerp_proxy.connection.connection import ConnectorBase -from openerp_proxy.utils import ustr +from .connection import ConnectorBase import openerp_proxy.exceptions as exceptions @@ -28,9 +27,6 @@ def __unicode__(self): def __str__(self): return unicode(self).encode('utf-8') - def _repr_pretty_(self): - return "TEST" - class JSONRPCMethod(object): """ Class wrapper around XML-RPC method to wrap xmlrpclib.Fault @@ -54,20 +50,21 @@ def __call__(self, *args): }, "id": random.randint(0, 1000000000), } - req = urllib2.Request(url=self.__url, data=json.dumps(data), headers={ - "Content-Type": "application/json", - }) - result = urllib2.urlopen(req) - content = result.read() try: - result = json.loads(content) + res = requests.post(self.__url, data=json.dumps(data), headers={ + "Content-Type": "application/json", + }) + except requests.exceptions.RequestException: + raise JSONRPCError("Cannot connect to url %s" % self.__url) + + try: + result = json.loads(res.text) except ValueError: info = { "original_url": self.__url, - "url": result.geturl(), - "info": result.info(), - "code": result.getcode(), - "content": content, + "url": res.url, + "code": res.status_code, + "content": res.text, } raise JSONRPCError("Cannot decode JSON: %s" % info) diff --git a/openerp_proxy/connection/xmlrpc.py b/openerp_proxy/connection/xmlrpc.py index ef55f95..3bc74b9 100644 --- a/openerp_proxy/connection/xmlrpc.py +++ b/openerp_proxy/connection/xmlrpc.py @@ -1,8 +1,8 @@ # python imports -import xmlrpclib +from six.moves import xmlrpc_client as xmlrpclib # project imports -from openerp_proxy.connection.connection import ConnectorBase +from .connection import ConnectorBase from openerp_proxy.utils import ustr import openerp_proxy.exceptions as exceptions diff --git a/openerp_proxy/core.py b/openerp_proxy/core.py index 180a6bc..d704431 100644 --- a/openerp_proxy/core.py +++ b/openerp_proxy/core.py @@ -1,5 +1,5 @@ # -*- coding: utf8 -*- -""" This module provides some classes to simplify acces to OpenERP server via xmlrpc. +""" This module provides some classes to simplify acces to Odoo server via xmlrpc. Some of these classes are may be not safe enough and should be used with carefully Example ussage of this module: @@ -46,37 +46,27 @@ ... 'assigned' """ +import six # project imports -from openerp_proxy.connection import get_connector -from openerp_proxy.exceptions import (Error, - ClientException, - LoginException) -from openerp_proxy.service import ServiceManager -from openerp_proxy.plugin import PluginManager +from .connection import get_connector +from .exceptions import LoginException +from .service import ServiceManager +from .plugin import PluginManager - -# Activate orm internal logic -# TODO: think about not enabling it by default, allowing users to choose what -# thay woudld like to use. Or simply create two entry points (one with all -# enabled by default and another with only basic stuff which may be useful for -# libraries that would like to get speed instead of better usability -import openerp_proxy.orm +# Enable ORM features +from . import orm from extend_me import Extensible -__all__ = ('ERPProxyException', 'Client', 'ERP_Proxy') - - -# Backward compatability -ERPProxyException = ClientException - +__all__ = ('Client',) +@six.python_2_unicode_compatible class Client(Extensible): """ - A simple class to connect ot ERP via RPC (XML-RPC, JSON-RPC) + A simple class to connect to Odoo instance via RPC (XML-RPC, JSON-RPC) Should be initialized with following arguments: :param str host: server host name to connect to @@ -94,7 +84,7 @@ class Client(Extensible): >>> cl = Client('host') >>> db2 = cl.login('dbname', 'user', 'password') - Allows access to ERP objects via dictionary syntax:: + Allows access to Odoo objects / models via dictionary syntax:: >>> db['sale.order'] Object ('sale.order') @@ -199,7 +189,7 @@ def server_version(self): @property def registered_objects(self): - """ Stores list of registered in ERP database objects + """ Stores list of registered in Odoo database objects """ return self.services['object'].get_registered_objects() @@ -290,7 +280,7 @@ def execute_wkf(self, object_name, signal, object_id): return result_wkf def get_obj(self, object_name): - """ Returns wraper around openERP object 'object_name' which is instance of Object + """ Returns wraper around Odoo object 'object_name' which is instance of Object :param object_name: name of an object to get wraper for :return: instance of Object which wraps choosen object @@ -353,7 +343,13 @@ def clean_caches(self): self.services.object.clean_caches() def __str__(self): - return "Client: %s" % self.get_url() - __repr__ = __str__ + return u"Client: %s" % self.get_url() + + def __repr__(self): + return str(self) -ERP_Proxy = Client + def __eq__(self, other): + if isinstance(other, Client): + return self.get_url() == other.get_url() + else: + return False diff --git a/openerp_proxy/ext/all.py b/openerp_proxy/ext/all.py index e69d3ea..6285adc 100644 --- a/openerp_proxy/ext/all.py +++ b/openerp_proxy/ext/all.py @@ -1,7 +1,6 @@ """ Just imports of all extensions """ -import openerp_proxy.ext.data import openerp_proxy.ext.field_datetime import openerp_proxy.ext.sugar import openerp_proxy.ext.workflow diff --git a/openerp_proxy/ext/data.py b/openerp_proxy/ext/data.py deleted file mode 100644 index e42283e..0000000 --- a/openerp_proxy/ext/data.py +++ /dev/null @@ -1,119 +0,0 @@ -""" This module provides extension which allows aditional -data manipulations, especialy filtering and grouping capabilities. -""" -from openerp_proxy.orm.record import ObjectRecords -from openerp_proxy.orm.record import RecordList, get_record_list -import collections -import functools - - -__all__ = ('ObjectData', 'RecordListData') - - -class RecordListData(RecordList): - """ Extend record list to add aditional method to work with lists of records - """ - - def group_by(self, grouper): - """ Groups all records in list by specifed grouper. - - :param grouper: field name or callable to group results by. - if function is passed, it should receive only - one argument - record instance, and result of - calling grouper will be used to group records. - :type grouper: string|callable(record) - - for example we have list of sale orders and want to group it by state:: - - # so_list - variable that contains list of sale orders selected - # by some criterias. so to group it by state we will do: - group = so_list.group_by('state') - for state, rlist in group.iteritems(): # Iterate over resulting dictionary - print state, rlist.length # Print state and amount of items with such state - - or imagine that we would like to groupe records by last letter of sale order number:: - - # so_list - variable that contains list of sale orders selected - # by some criterias. so to group it by last letter of sale - # order name we will do: - group = so_list.group_by(lambda so: so.name[-1]) - for letter, rlist in group.iteritems(): # Iterate over resulting dictionary - print letter, rlist.length # Print state and amount of items with such state - """ - cls_init = functools.partial(get_record_list, - self.object, - ids=[], - cache=self._cache) - res = collections.defaultdict(cls_init) - for record in self.records: - if isinstance(grouper, basestring): - key = record[grouper] - elif callable(grouper): - key = grouper(record) - - res[key].append(record) - return res - - def filter(self, func): - """ Filters items using *func*. - - :param func: callable to check if record should be included in result. - also *openerp_proxy.utils.r_eval* may be used - :type func: callable(record)->bool - :return: RecordList which contains records that matches results - :rtype: RecordList - """ - result_ids = [record.id for record in self.records if func(record)] - return get_record_list(self.object, ids=result_ids, cache=self._cache) - - -# TODO: implement some class wrapper to by default load only count of domains, -# and by some method load ids, or records if required. this will allow to -# work better with data when accessing root object showing all groups and -# amounts of objects within, but when accessing some object we could get -# records related to that group to analyse them. -class ObjectData(ObjectRecords): - """ Provides aditional methods to work with data - """ - - def data__get_grouped(self, group_rules, count=False): - """ Returns dictionary with grouped data. if count=True returns only amount of items found for rule - otherwise returns list of records found for each rule - - :param group_rules: dictionary with keys=group_names and values are domains or other dictionary - with domains. - For example - - :: - - group_rules = {'g1': [('state','=','done')], - 'g2': { - '__sub_domain': [('partner_id','=',5)], - 'total': [], - 'done': [('state', '=', 'done')], - 'cancel': [('state', '=', 'cancel')] - }} - - Each group may contain '__sub_domain' field with domain applied to all - items of group - :type group_rules: dict - :param count: if True then result dictinary will contain only counts - otherwise each group in result dictionary will contain RecordList of records found - :type count: boolean (default: False) - :return: dictionary like 'group_rules' but with domains replaced by search result (RecordList instance). - """ - result = {} - sub_domain = group_rules.pop('__sub_domain', []) - for key, value in group_rules.iteritems(): - if isinstance(value, (list, tuple)): # If value is domain - domain = sub_domain + value - result[key] = self.search_records(domain, count=count) - elif isinstance(value, dict): # if value is subgroup of domains - _sub_domain = sub_domain + value.get('__sub_domain', []) - if _sub_domain: - value['__sub_domain'] = _sub_domain - result[key] = self.data__get_grouped(value, count=count) - else: - raise TypeError("Unsupported type for 'group_rules' value for key %s: %s" % (key, type(value))) - return result - diff --git a/openerp_proxy/ext/repr.py b/openerp_proxy/ext/repr.py index 49a1943..1aaa631 100644 --- a/openerp_proxy/ext/repr.py +++ b/openerp_proxy/ext/repr.py @@ -7,6 +7,7 @@ # TODO: rename to IPython or something like that +import six import csv import tempfile @@ -15,6 +16,7 @@ from openerp_proxy.orm.object import Object from openerp_proxy.core import Client from openerp_proxy.utils import AttrDict +from openerp_proxy.session import Session from IPython.display import HTML, FileLink @@ -211,7 +213,7 @@ def update(self, fields=None, caption=None, highlighters=None, **kwargs): for field in fields: if isinstance(field, HField): self._fields.append(field) - elif isinstance(field, basestring): + elif isinstance(field, six.string_types): self._fields.append(HField(field)) elif callable(field): self._fields.append(HField(field)) @@ -429,13 +431,13 @@ def as_html(self, *fields): if not fields: fields = sorted((HField(col_name, name=col_data['string']) - for col_name, col_data in self._columns_info.iteritems() + for col_name, col_data in self._columns_info.items() if col_name in self._object.simple_fields), key=lambda x: _(x)) self.read() else: # TODO: implement in better way this prefetching - read_fields = (f.split('.')[0] for f in fields if isinstance(f, basestring) and f.split('.')[0] in self._columns_info) + read_fields = (f.split('.')[0] for f in fields if isinstance(f, six.string_types) and f.split('.')[0] in self._columns_info) prefetch_fields = [f for f in read_fields if f not in self._data] self.read(prefetch_fields) @@ -443,7 +445,7 @@ def as_html(self, *fields): for field in fields: if isinstance(field, HField): parsed_fields.append(field) - elif isinstance(field, basestring): + elif isinstance(field, six.string_types): parsed_fields.append(HField(field)) else: raise TypeError("Bad type of field %s" % repr(field)) @@ -528,7 +530,7 @@ def as_html_table(self, fields=None): """ fields = self.default_fields if fields is None else fields info_struct = [{'name': key, - 'info': val} for key, val in self.iteritems()] + 'info': val} for key, val in self.items()] info_struct.sort(key=lambda x: x['name']) return HTMLTable(info_struct, fields, caption=u'Fields for %s' % _(self._object.name)) @@ -620,3 +622,37 @@ def to_row(header, val): table = ttable % (caption + data) return html % (table + help_text) + + +class IPYSession(Session): + def _repr_html_(self): + """ Provides HTML representation of session (Used for IPython) + """ + from openerp_proxy.utils import ustr as _ + + def _get_data(): + for url in self._databases.keys(): + index = self._index_url(url) + aliases = (_(al) for al, aurl in self.aliases.items() if aurl == url) + yield (url, index, u", ".join(aliases)) + ttable = u"%s
                                  " + trow = u"%s" + tdata = u"%s" + caption = u"Previous connections" + hrow = u"DB URLDB IndexDB Aliases" + help_text = (u"
                                  " + u"To get connection just call
                                    " + u"
                                  • session.aliase
                                  • " + u"
                                  • session[index]
                                  • " + u"
                                  • session[aliase]
                                  • " + u"
                                  • session[url]
                                  • " + u"
                                  • session.get_db(url|index|aliase)
                                  • " + u"
                                  ") + + data = u"" + for row in _get_data(): + data += trow % (u''.join((tdata % i for i in row))) + + table = ttable % (caption + hrow + data) + + return u"
                                  %s %s
                                  " % (table, help_text) diff --git a/openerp_proxy/ext/sugar.py b/openerp_proxy/ext/sugar.py index 9cd740f..b4d95b4 100644 --- a/openerp_proxy/ext/sugar.py +++ b/openerp_proxy/ext/sugar.py @@ -94,7 +94,7 @@ def __call__(self, *args, **kwargs): # no arguments, only keyword arguments passsed, # so build domain based on keyword arguments if name is None: - domain = [(k, '=', v) for k, v in kwargs.iteritems()] + domain = [(k, '=', v) for k, v in kwargs.items()] return self.search_records(domain, *args) # normal domain passed, then just forward all arguments and diff --git a/openerp_proxy/ext/workflow.py b/openerp_proxy/ext/workflow.py index 17bdf86..aa7255e 100644 --- a/openerp_proxy/ext/workflow.py +++ b/openerp_proxy/ext/workflow.py @@ -5,7 +5,8 @@ Also it provides simple methods to easily send workflow signals to records from Object and Record interfaces. """ - +import numbers +import six from openerp_proxy.orm.record import Record from openerp_proxy.orm.record import ObjectRecords from openerp_proxy.exceptions import ObjectException @@ -42,8 +43,8 @@ def workflow(self): def workflow_signal(self, obj_id, signal): """ Triggers specified signal for object's workflow """ - assert isinstance(obj_id, (int, long)), "obj_id must be integer" - assert isinstance(signal, basestring), "signal must be string" + assert isinstance(obj_id, numbers.Integral), "obj_id must be integer" + assert isinstance(signal, six.string_types), "signal must be string" return self.service.execute_wkf(self.name, signal, obj_id) diff --git a/openerp_proxy/main.py b/openerp_proxy/main.py new file mode 100644 index 0000000..57f1bb1 --- /dev/null +++ b/openerp_proxy/main.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + + +HELP_HEADER = """ + Usage: + >>> db = session.connect() + >>> so_obj = db['sale.orderl'] # get object + >>> dir(so_obj) # Thid will show all default methods of object + >>> so_id = 123 # ID of sale order + >>> so_obj.read(so_id) + >>> so_obj.write([so_id], {'note': 'Test'}) + >>> sm_obj = db['stock.move'] + >>> + >>> # check availability of stock move (call server-side method) + >>> sm_obj.check_assign([move_id1, move_id2,...]) + + Available objects in context: + Client - class that represents single Odoo / OpenERP database and + provides methods to work with data. Session.connect method usualy + returns instances of this class. + session - represents session of client, stores in home directory list + of databases user works with, to simplify work. It is simpler + to get list of databases you have worked with previously on program + start, and to connect to them without remembrering hosts, users, ports + and other unneccesary information + + Databases You previously worked with: %(databases)s + + Aliases: %(aliases)s + + (Use index or url or aliase for session: session[1] or session[url] or session[aliase]) +""" + + +def main(): + """ Entry point for running as standalone APP + """ + from .session import Session + from .core import Client + + session = Session() + + header_databases = "\n" + for index, url in session.index.items(): + header_databases += " - [%3s] %s\n" % (index, url) + + header_aliases = "\n" + for aliase, url in session.aliases.items(): + header_aliases += " - %7s: %s\n" % (aliase, url) + + header = HELP_HEADER % {'databases': header_databases, 'aliases': header_aliases} + + _locals = { + 'Client': Client, + 'session': session, + } + try: + from IPython import embed + embed(user_ns=_locals, header=header) + except ImportError: + from code import interact + interact(local=_locals, banner=header) + + session.save() + +if __name__ == '__main__': + main() + diff --git a/openerp_proxy/orm/cache.py b/openerp_proxy/orm/cache.py index 911ae2c..acdad1e 100644 --- a/openerp_proxy/orm/cache.py +++ b/openerp_proxy/orm/cache.py @@ -1,6 +1,9 @@ #import openerp_proxy.orm.record +import six +import numbers import collections -__all__ = ('empty_cache') + +__all__ = ('empty_cache',) class ObjectCache(dict): @@ -32,7 +35,7 @@ def update_keys(self, keys): # and difference calls) self.update({cid: {'id': cid} for cid in keys}) else: - self.update({cid: {'id': cid} for cid in set(keys).difference(self.viewkeys())}) + self.update({cid: {'id': cid} for cid in set(keys).difference(six.viewkeys(self))}) return self def update_context(self, new_context): @@ -51,7 +54,7 @@ def update_context(self, new_context): def get_ids_to_read(self, field): """ Return list of ids, that have no specified field in cache """ - return [key for key, val in self.viewitems() if field not in val] + return [key for key, val in six.viewitems(self) if field not in val] def cache_field(self, rid, ftype, field_name, value): """ This method impelment additional caching functionality, @@ -66,9 +69,9 @@ def cache_field(self, rid, ftype, field_name, value): if value and ftype == 'many2one': rcache = self._root_cache[self._object.columns_info[field_name]['relation']] - if isinstance(value, (int, long)): + if isinstance(value, numbers.Integral): rcache[value] # internal dict {'id': key} will be created by default (see ObjectCache) - elif isinstance(value, (list, tuple)): + elif isinstance(value, collections.Iterable): rcache[value[0]]['__name_get_result'] = value[1] elif value and ftype in ('many2many', 'one2many'): rcache = self._root_cache[self._object.columns_info[field_name]['relation']] @@ -110,8 +113,8 @@ def prefetch_fields(self, fields): to_prefetch, related = self.parse_prefetch_fields(fields) col_info = self._object.columns_info - for data in self._object.read(self.keys(), to_prefetch): - for field, value in data.iteritems(): + for data in self._object.read(list(self), to_prefetch): + for field, value in data.items(): # Fill related cache ftype = col_info.get(field, {}).get('type', None) @@ -119,7 +122,7 @@ def prefetch_fields(self, fields): if related: # TODO: think how to avoid infinite recursion and double reads - for obj_name, rfields in related.viewitems(): + for obj_name, rfields in related.items(): self._root_cache[obj_name].prefetch_fields(rfields) diff --git a/openerp_proxy/orm/object.py b/openerp_proxy/orm/object.py index 470adda..2056b28 100644 --- a/openerp_proxy/orm/object.py +++ b/openerp_proxy/orm/object.py @@ -1,3 +1,4 @@ +import six from extend_me import ExtensibleByHashType from openerp_proxy.utils import AttrDict @@ -24,7 +25,8 @@ def get_object(proxy, name): # TODO: think about connecting it to service instead of Proxy -class Object(object): +@six.python_2_unicode_compatible +class Object(six.with_metaclass(ObjectType)): """ Base class for all Objects Provides simple interface to remote osv.osv objects @@ -33,7 +35,6 @@ class Object(object): sale_obj = Object(erp, 'sale.order') sale_obj.search([('state','not in',['done','cancel'])]) """ - __metaclass__ = ObjectType def __init__(self, service, object_name): self._service = service @@ -62,13 +63,13 @@ def proxy(self): # Overriden to add some standard method to be available in introspection # Useful for IPython auto completition def __dir__(self): - res = dir(super(Object, self)) + res = dir(super(self.__class__, self)) res.extend(['read', 'search', 'write', 'unlink', 'create']) return res def __getattr__(self, name): def method_wrapper(object_name, method_name): - """ Wraper around ERP objects's methods. + """ Wraper around Odoo objects's methods. for internal use. It is used in Object class. @@ -87,8 +88,10 @@ def wrapper(*args, **kwargs): return getattr(self, name) def __str__(self): - return "Object ('%s')" % self.name - __repr__ = __str__ + return u"Object ('%s')" % self.name + + def __repr__(self): + return str(self) def __eq__(self, other): return self.name == other.name and self.proxy == other.proxy diff --git a/openerp_proxy/orm/record.py b/openerp_proxy/orm/record.py index 7d788cf..0b444d2 100644 --- a/openerp_proxy/orm/record.py +++ b/openerp_proxy/orm/record.py @@ -4,8 +4,11 @@ from openerp_proxy.orm.cache import empty_cache from extend_me import ExtensibleType -import collections +import six import abc +import numbers +import functools +import collections __all__ = ( @@ -37,7 +40,8 @@ def get_record(obj, rid, cache=None, context=None): return RecordMeta.get_object(obj, rid, cache=cache, context=context) -class Record(object): +@six.python_2_unicode_compatible +class Record(six.with_metaclass(RecordMeta, object)): """ Base class for all Records Constructor @@ -50,12 +54,11 @@ class Record(object): Note, to create instance of cache call *empty_cache* """ - __metaclass__ = RecordMeta __slots__ = ['__dict__', '_object', '_cache', '_lcache', '_id'] def __init__(self, obj, rid, cache=None, context=None): assert isinstance(obj, Object), "obj should be Object" - assert isinstance(rid, (int, long)), "rid must be int" + assert isinstance(rid, numbers.Integral), "rid must be int" self._id = rid self._object = obj @@ -66,7 +69,7 @@ def __init__(self, obj, rid, cache=None, context=None): def __dir__(self): # TODO: expose also object's methods - res = dir(super(Record, self)) + res = dir(super(self.__class__, self)) res.extend(self._columns_info.keys()) res.extend(['read', 'search', 'write', 'unlink']) return res @@ -120,16 +123,13 @@ def _name(self): """ if self._data.get('__name_get_result', None) is None: lcache = self._lcache - data = self._object.name_get(lcache.keys(), context=self.context) + data = self._object.name_get(list(lcache), context=self.context) for _id, name in data: lcache[_id]['__name_get_result'] = name - return self._data.get('__name_get_result', 'ERROR') - - def __unicode__(self): - return u"R(%s, %s)[%s]" % (self._object.name, self.id, ustr(self._name)) + return self._data.get('__name_get_result', u'ERROR') def __str__(self): - return unicode(self).encode('utf-8') + return u"R(%s, %s)[%s]" % (self._object.name, self.id, ustr(self._name)) def __repr__(self): return str(self) @@ -144,7 +144,7 @@ def __eq__(self, other): if isinstance(other, Record): return other.id == self._id - if isinstance(other, (int, long)): + if isinstance(other, numbers.Integral): return self._id == other return False @@ -217,7 +217,7 @@ def read(self, fields=None, context=None, multi=False): :rtype: dict """ ctx = {} if self.context is None else self.context.copy() - args = [self._lcache.keys()] if multi else [[self.id]] + args = [list(self._lcache)] if multi else [[self.id]] kwargs = {} @@ -255,7 +255,8 @@ def get_record_list(obj, ids=None, fields=None, cache=None, context=None): return RecordListMeta.get_object(obj, ids, fields=fields, cache=cache, context=context) -class RecordList(collections.MutableSequence): +@six.python_2_unicode_compatible +class RecordList(six.with_metaclass(RecordListMeta, collections.MutableSequence)): """Class to hold list of records with some extra functionality :param obj: instance of Object to make this list related to @@ -270,8 +271,6 @@ class RecordList(collections.MutableSequence): :type context: dict """ - __metaclass__ = RecordListMeta - __slots__ = ('_object', '_cache', '_lcache', '_records') # TODO: expose object's methods via implementation of __dir__ @@ -347,12 +346,15 @@ def __getitem__(self, index): if isinstance(index, slice): # Note no context passed, because it is stored in cache return get_record_list(self.object, - ids=(r.id for r in self._records[index]), + ids=[r.id for r in self._records[index]], cache=self._cache) return self._records[index] def __setitem__(self, index, value): - self._records[index] = value + if isinstance(value, Record): + self._records[index] = value + else: + raise ValueError("In 'RecordList[index] = value' operation, value must be instance of Record") def __delitem__(self, index): del self._records[index] @@ -364,7 +366,7 @@ def __len__(self): return self.length def __contains__(self, item): - if isinstance(item, (int, long)): + if isinstance(item, numbers.Integral): return item in self.ids if isinstance(item, Record): return item in self._records @@ -373,13 +375,13 @@ def __contains__(self, item): def insert(self, index, item): """ Insert record to list - :param item: Record instance to be inserted into list. if int or long passed, it considered to be ID of record - :type item: Record|int|long + :param item: Record instance to be inserted into list. if int passed, it considered to be ID of record + :type item: Record|int :param int index: position where to place new element :return: self :rtype: RecordList """ - assert isinstance(item, (Record, int, long)), "Only Record or int or long instances could be added to list" + assert isinstance(item, (Record, numbers.Integral)), "Only Record or int instances could be added to list" if isinstance(item, Record): self._records.insert(index, item) else: @@ -390,13 +392,15 @@ def insert(self, index, item): # present in this RecordList def __getattr__(self, name): method = getattr(self.object, name) - res = wpartial(method, self.ids, context=self.context) - #setattr(self, name, res) # commented because of __slots__ + kwargs = {} if self.context is None else {'context': self.context} + res = wpartial(method, self.ids, **kwargs) return res def __str__(self): - return "RecordList(%s): length=%s" % (self.object.name, self.length) - __repr__ = __str__ + return u"RecordList(%s): length=%s" % (self.object.name, self.length) + + def __repr__(self): + return str(self) def refresh(self): """ Cleanup data caches. next try to get data will cause rereading of it @@ -408,11 +412,69 @@ def refresh(self): record.refresh() return self - def sort(self, cmp=None, key=None, reverse=False): + def sort(self, *args, **kwargs): """ sort(cmp=None, key=None, reverse=False) -- inplace sort cmp(x, y) -> -1, 0, 1 + + Note, that 'cmp' argument, not available for python 3 + + :return: self """ - return self._records.sort(cmp=cmp, key=key, reverse=reverse) + self._records.sort(*args, **kwargs) + return self + + def group_by(self, grouper): + """ Groups all records in list by specifed grouper. + + :param grouper: field name or callable to group results by. + if callable is passed, it should receive only + one argument - record instance, and result of + calling grouper will be used as key to group records by. + :type grouper: string|callable(record) + + for example we have list of sale orders and want to group it by state:: + + # so_list - variable that contains list of sale orders selected + # by some criterias. so to group it by state we will do: + group = so_list.group_by('state') + for state, rlist in group.iteritems(): # Iterate over resulting dictionary + print state, rlist.length # Print state and amount of items with such state + + or imagine that we would like to groupe records by last letter of sale order number:: + + # so_list - variable that contains list of sale orders selected + # by some criterias. so to group it by last letter of sale + # order name we will do: + group = so_list.group_by(lambda so: so.name[-1]) + for letter, rlist in group.iteritems(): # Iterate over resulting dictionary + print letter, rlist.length # Print state and amount of items with such state + """ + cls_init = functools.partial(get_record_list, + self.object, + ids=[], + cache=self._cache) + res = collections.defaultdict(cls_init) + for record in self.records: + if isinstance(grouper, six.string_types): + key = record[grouper] + elif callable(grouper): + key = grouper(record) + + res[key].append(record) + return res + + def filter(self, func): + """ Filters items using *func*. + + :param func: callable to check if record should be included in result. + also *openerp_proxy.utils.r_eval* may be used + :type func: callable(record)->bool + :return: RecordList which contains records that matches results + :rtype: RecordList + """ + return get_record_list(self.object, + ids=[r.id for r in self.records if func(r)], + cache=self._cache) def copy(self, context=None, new_cache=False): """ Returns copy of this list, possibly with modified context @@ -474,7 +536,11 @@ def search(self, domain, *args, **kwargs): :returns: list of IDs found :rtype: list of integers """ - kwargs['context'] = self._new_context(kwargs.get('context', None)) + ctx = self._new_context(kwargs.get('context', None)) + + if ctx is not None: + kwargs['context'] = ctx + return self.object.search([('id', 'in', self.ids)] + domain, *args, **kwargs) def search_records(self, domain, *args, **kwargs): @@ -483,7 +549,11 @@ def search_records(self, domain, *args, **kwargs): :returns: RecordList of records found :rtype: RecordList instance """ - kwargs['context'] = self._new_context(kwargs.get('context', None)) + ctx = self._new_context(kwargs.get('context', None)) + + if ctx is not None: + kwargs['context'] = ctx + return self.object.search_records([('id', 'in', self.ids)] + domain, *args, **kwargs) def read(self, fields=None, context=None): @@ -493,7 +563,10 @@ def read(self, fields=None, context=None): kwargs = {} args = [] - kwargs['context'] = self._new_context(kwargs.get('context', None)) + ctx = self._new_context(kwargs.get('context', None)) + + if ctx is not None: + kwargs['context'] = ctx if fields is not None: args.append(fields) @@ -521,7 +594,7 @@ def _get_many2one_rel_obj(self, name, rel_data, cached=True): if name not in self._related_objects or not cached: if rel_data: # Do not forged about relations in form [id, name] - rel_id = rel_data[0] if isinstance(rel_data, (list, tuple)) else rel_data + rel_id = rel_data[0] if isinstance(rel_data, collections.Iterable) else rel_data rel_obj = self._service.get_obj(self._columns_info[name]['relation']) self._related_objects[name] = get_record(rel_obj, rel_id, cache=self._cache, context=self.context) @@ -559,7 +632,7 @@ def refresh(self): rel_objects = self._related_objects self._related_objects = {} - for rel in rel_objects.itervalues(): + for rel in rel_objects.values(): if isinstance(rel, (Record, RecordList)): rel.refresh() # both, Record and RecordList objects have 'refresh* method return self @@ -658,17 +731,15 @@ def read_records(self, ids, fields=None, context=None, cache=None): >>> for order in data: order.write({'note': 'order data is %s'%order.data}) """ - assert isinstance(ids, (int, long, list, tuple)), "ids must be instance of (int, long, list, tuple)" - - if isinstance(ids, (int, long)): + if isinstance(ids, numbers.Integral): record = get_record(self, ids, context=context) if fields is not None: record.read(fields) # read specified fields return record - if isinstance(ids, (list, tuple)): + if isinstance(ids, collections.Iterable): return get_record_list(self, ids, fields=fields, context=context) - raise ValueError("Wrong type for ids args") + raise ValueError("Wrong type for ids argument: %s" % type(ids)) def browse(self, *args, **kwargs): """ Aliase to *read_records* method. In most cases same as serverside *browse* diff --git a/openerp_proxy/orm/service.py b/openerp_proxy/orm/service.py index 5b37771..e437411 100644 --- a/openerp_proxy/orm/service.py +++ b/openerp_proxy/orm/service.py @@ -1,6 +1,8 @@ from openerp_proxy.service.object import ObjectService from openerp_proxy.orm.object import get_object +__all__ = ('Service',) + class Service(ObjectService): """ Service class to simplify interaction with 'object' service. @@ -13,7 +15,7 @@ def __init__(self, *args, **kwargs): self.__objects = {} # cached objects def get_obj(self, object_name): - """ Returns wraper around OpenERP object 'object_name' which is instance of Object + """ Returns wraper around Odoo object 'object_name' which is instance of Object :param object_name: name of an object to get wraper for :type object_name: string @@ -24,7 +26,7 @@ def get_obj(self, object_name): return self.__objects[object_name] if object_name not in self.get_registered_objects(): - raise ValueError("There is no object named '%s' in ERP" % object_name) + raise ValueError("There is no object named '%s'" % object_name) obj = get_object(self, object_name) self.__objects[object_name] = obj diff --git a/openerp_proxy/plugin.py b/openerp_proxy/plugin.py index 494523a..0500d4f 100644 --- a/openerp_proxy/plugin.py +++ b/openerp_proxy/plugin.py @@ -1,9 +1,11 @@ # Python imports - +import six import extend_me +PluginMeta = extend_me.ExtensibleByHashType._('Plugin', hashattr='name') + -class Plugin(object): +class Plugin(six.with_metaclass(PluginMeta)): """ Base class for all plugins, extensible by name (uses metaclass extend_me.ExtensibleByHashType) @@ -31,7 +33,6 @@ def get_sign_state(self): This plugin will automaticaly register itself in system, when module which contains it will be imported. """ - __metaclass__ = extend_me.ExtensibleByHashType._('Plugin', hashattr='name') def __init__(self, erp_proxy): self._erp_proxy = erp_proxy @@ -64,7 +65,7 @@ def test(self): return self.proxy.get_url() -class PluginManager(object): +class PluginManager(extend_me.Extensible): """ Class that holds information about all plugins :param erp_proxy: instance of Client to bind plugins to @@ -88,7 +89,7 @@ def __getitem__(self, name): try: pluginCls = type(Plugin).get_class(name) except ValueError as e: - raise KeyError(e.message) + raise KeyError(str(e)) plugin = pluginCls(self.__erp_proxy) self.__plugins[name] = plugin diff --git a/openerp_proxy/plugins/graph.py b/openerp_proxy/plugins/graph.py index 817be20..3f3cc21 100644 --- a/openerp_proxy/plugins/graph.py +++ b/openerp_proxy/plugins/graph.py @@ -7,7 +7,7 @@ try: import pydot except ImportError: - print "PyDot not installed!!!" + print("PyDot not installed!!!") class Model(Extensible): diff --git a/openerp_proxy/plugins/module_utils.py b/openerp_proxy/plugins/module_utils.py index 280feef..578ebe9 100644 --- a/openerp_proxy/plugins/module_utils.py +++ b/openerp_proxy/plugins/module_utils.py @@ -13,12 +13,16 @@ class Meta: def upgrade(self, ids): """ Immediatly upgrades module """ - return self.button_immediate_upgrade(ids) + res = self.button_immediate_upgrade(ids) + self.proxy.clean_caches() # because new models may appear in DB, so registered_objects shoud be refreshed + return res def install(self, ids): """ Immediatly install module """ - return self.button_immediate_install(ids) + res = self.button_immediate_install(ids) + self.proxy.clean_caches() # because new models may appear in DB, so registered_objects shoud be refreshed + return res class ModuleUtils(Plugin): diff --git a/openerp_proxy/service/db.py b/openerp_proxy/service/db.py index 7e4e41b..0a0a06a 100644 --- a/openerp_proxy/service/db.py +++ b/openerp_proxy/service/db.py @@ -30,7 +30,7 @@ def create_db(self, password, dbname, demo=False, lang='en_US', admin_password=' from openerp_proxy.core import Client # requires server version >= 6.1 - if self.server_version >= parse_version('6.1'): + if self.server_version() >= parse_version('6.1'): self.create_database(password, dbname, demo, lang, admin_password) else: # for other server versions process_id = self.create(password, dbname, demo, lang, admin_password) @@ -67,4 +67,4 @@ def server_version(self): (Already parsed with pkg_resources.parse_version) """ - return parse_version(super(DBService, self).server_version()) + return parse_version(self._service.server_version()) diff --git a/openerp_proxy/service/object.py b/openerp_proxy/service/object.py index 80f90ed..fb45420 100644 --- a/openerp_proxy/service/object.py +++ b/openerp_proxy/service/object.py @@ -49,7 +49,7 @@ def execute_wkf(self, object_name, signal, object_id): :param str object_name: name of object/model to trigger workflow on :param str signal: name of signal to send to workflow - :param int|long object_id: ID of document (record) to send signal to + :param int object_id: ID of document (record) to send signal to """ result_wkf = self._service.exec_workflow(self.proxy.dbname, self.proxy.uid, self.proxy._pwd, object_name, signal, object_id) return result_wkf diff --git a/openerp_proxy/service/report.py b/openerp_proxy/service/report.py index bab18ec..86226f6 100644 --- a/openerp_proxy/service/report.py +++ b/openerp_proxy/service/report.py @@ -1,3 +1,5 @@ +import six +import numbers from openerp_proxy.service.service import ServiceBase from extend_me import ExtensibleType @@ -8,7 +10,7 @@ class ReportError(Error): pass -class ReportResult(object): +class ReportResult(six.with_metaclass(ExtensibleType._('ReportResult'), object)): """ Just a simple and extensible wrapper on report result As variant of usage - wrap result returned by server methods @@ -17,7 +19,6 @@ class ReportResult(object): ReportResult(report_get(report_id)) """ - __metaclass__ = ExtensibleType._('ReportResult') def __init__(self, result, path=None): self._orig_result = result @@ -104,7 +105,7 @@ def available_reports(self): def _prepare_report_data(self, model, ids, report_type): """ Performs preparation of data """ - ids = [ids] if isinstance(ids, (int, long)) else ids + ids = [ids] if isinstance(ids, numbers.Integral) else ids return { 'model': model, 'id': ids[0], @@ -125,7 +126,7 @@ def report(self, report_name, model, ids, report_type='pdf', context=None): :rtype: int """ context = {} if context is None else context - ids = [ids] if isinstance(ids, (int, long)) else ids + ids = [ids] if isinstance(ids, numbers.Integral) else ids data = self._prepare_report_data(model, ids, report_type) return self._service.report(self.proxy.dbname, self.proxy.uid, @@ -185,7 +186,7 @@ def render_report(self, report_name, model, ids, report_type='pdf', context=None :rtype: dict|ReportResult """ context = {} if context is None else context - ids = [ids] if isinstance(ids, (int, long)) else ids + ids = [ids] if isinstance(ids, numbers.Integral) else ids data = self._prepare_report_data(model, ids, report_type) if wrap_result: diff --git a/openerp_proxy/service/service.py b/openerp_proxy/service/service.py index 861043f..f9e878e 100644 --- a/openerp_proxy/service/service.py +++ b/openerp_proxy/service/service.py @@ -1,9 +1,11 @@ -from extend_me import ExtensibleByHashType +import six +from extend_me import (ExtensibleByHashType, + Extensible) __all__ = ('get_service_class', 'ServiceBase', 'ServiceManager') -class ServiceManager(object): +class ServiceManager(Extensible): """ Class to hold services related to specific proxy and to automaticaly clean service cached on update of service classes @@ -43,7 +45,7 @@ def __dir__(self): def list(self): """ Returns list of all registered services """ - return list(set(self.__services.keys() + ServiceType.get_registered_names())) + return list(set(list(self.__services.keys()) + ServiceType.get_registered_names())) def get_service(self, name): """ Returns instance of service with specified name @@ -67,6 +69,9 @@ def __getattr__(self, name): def __getitem__(self, name): return self.get_service(name) + def __contains__(self, name): + return name in self.list + ServiceType = ExtensibleByHashType._('Service', hashattr='name') @@ -77,10 +82,9 @@ def get_service_class(name): return ServiceType.get_class(name, default=True) -class ServiceBase(object): +class ServiceBase(six.with_metaclass(ServiceType, object)): """ Base class for all Services """ - __metaclass__ = ServiceType def __init__(self, service, erp_proxy): self._erp_proxy = erp_proxy diff --git a/openerp_proxy/session.py b/openerp_proxy/session.py index 23eba5c..5a437b8 100644 --- a/openerp_proxy/session.py +++ b/openerp_proxy/session.py @@ -1,18 +1,21 @@ -import json +import numbers import os.path import sys import pprint from getpass import getpass +from extend_me import Extensible # project imports -from core import Client +from .core import Client +from .utils import (json_read, + json_write, + xinput) -__all__ = ('ERP_Session', 'Session', 'IPYSession') +__all__ = ('Session',) -# TODO: completly refactor -class Session(object): +class Session(Extensible): """ Simple session manager which allows to manage databases easier This class stores information about databases You used in home @@ -35,6 +38,7 @@ def __init__(self, data_file='~/.openerp_proxy.json'): """ """ self.data_file = os.path.expanduser(data_file) + self._databases = {} # key: url; value: instance of DB or dict with init args self._db_aliases = {} # key: aliase name; value: url self._options = {} @@ -43,41 +47,39 @@ def __init__(self, data_file='~/.openerp_proxy.json'): self._db_index_rev = {} # key: url; value: index self._db_index_counter = 0 - self._start_up_imports = [] # list of modules/packages to be imported at startup - - self._extra_paths = set() - if os.path.exists(self.data_file): - with open(self.data_file, 'rt') as json_data: - data = json.load(json_data) + data = json_read(self.data_file) - self._databases = data.get('databases', {}) - self._db_aliases = data.get('aliases', {}) - self._options = data.get('options', {}) + self._databases = data.get('databases', {}) + self._db_aliases = data.get('aliases', {}) + self._options = data.get('options', {}) - self._init_paths(data) - self._init_start_up_imports(data) + for path in self.extra_paths: + self.add_path(path) - def _init_start_up_imports(self, data): - """ Loads list of modules/packages names to be imported at start-up, - saved in previous session + for module in self.start_up_imports: # pragma: no cover + try: + __import__(module) + except ImportError: + # TODO: implement some logging + pass - :param data: dictionary with data read from saved session file + @property + def extra_paths(self): + """ List of extra pyhton paths, used by this session """ - self._start_up_imports += data.get('start_up_imports', []) - self._start_up_imports = list(set(self._start_up_imports)) - for i in self._start_up_imports: - try: - __import__(i) - except ImportError: - # TODO: implement some logging - pass - - def _init_paths(self, data): - """ This method initializes aditional python paths saved in session + return self.option('extra_paths', default=[]) + + @property + def start_up_imports(self): + """ List of start-up imports + + If You want some module to be automaticaly imported on + when session starts, that just add it to this list:: + + session.start_up_imports.append('openerp_proxy.ext.sugar') """ - for path in data.get('extra_paths', []): - self.add_path(path) + return self.option('start_up_imports', default=[]) def add_path(self, path): """ Adds extra path to python import path. @@ -88,11 +90,14 @@ def add_path(self, path): Note: this way path will be saved in session """ + # TODO: rewrite extrapaths logic with custom importers. It will be more + # pythonic if path not in sys.path: sys.path.append(path) - self._extra_paths.add(path) + if path not in self.extra_paths: + self.extra_paths.append(path) - def option(self, opt, val=None): + def option(self, opt, val=None, default=None): """ Get or set option. if *val* is passed, *val* will be set as value for option, else just option value will be returned @@ -110,7 +115,9 @@ def option(self, opt, val=None): """ if val is not None: self._options[opt] = val - return self._options.get(opt, None) + elif opt not in self._options and default is not None: + self._options[opt] = default + return self._options.get(opt, default) @property def aliases(self): @@ -144,14 +151,17 @@ def aliase(self, name, val): :return: unchanged val """ - if val in self._databases: - self._db_aliases[name] = val - elif val in self.index: - self._db_aliases[name] = self.index[val] - elif isinstance(val, Client): - self._db_aliases[name] = val.get_url() + if isinstance(val, Client): + url = val.get_url() + elif isinstance(val, numbers.Integral) and val in self.index: + url = self.index[val] + else: + url = val + + if url in self._databases: + self._db_aliases[name] = url else: - raise ValueError("Bad value type") + raise ValueError("Bad value type: %s" % val) return val @@ -164,17 +174,6 @@ def index(self): self._index_url(url) return dict(self._db_index) - @property - def start_up_imports(self): - """ List of start-up imports - - If You want some module to be automaticaly imported on - when session starts, that just add it to this list:: - - session.start_up_imports.append('openerp_proxy.ext.sugar') - """ - return self._start_up_imports - def _index_url(self, url): """ Returns index of specified URL, or adds it to store assigning new index @@ -187,23 +186,19 @@ def _index_url(self, url): self._db_index_rev[url] = self._db_index_counter return self._db_index_counter - def _add_db(self, url, db): - """ Add database to history - """ - self._databases[url] = db - self._index_url(url) - def add_db(self, db): """ Add db to session. param db: database (client instance) to be added to session type db: Client instance """ - self._add_db(db.get_url(), db) + url = db.get_url() + self._databases[url] = db + self._index_url(url) def get_db(self, url_or_index, **kwargs): """ Returns instance of Client object, that represents single - OpenERP database it connected to, specified by passed index (integer) or + Odoo database it connected to, specified by passed index (integer) or url (string) of database, previously saved in session. :param url_or_index: must be integer (if index) or string (if url). this parametr @@ -219,7 +214,7 @@ def get_db(self, url_or_index, **kwargs): session.get_db('xml-rpc://katyukha@erp.jbm.int:8069/jbm0') # using url session.get_db('my_db') # using aliase """ - if isinstance(url_or_index, (int, long)): + if isinstance(url_or_index, numbers.Integral): url = self.index[url_or_index] else: url = self._db_aliases.get(url_or_index, url_or_index) @@ -236,12 +231,15 @@ def get_db(self, url_or_index, **kwargs): if 'pwd' not in ep_args: if self.option('store_passwords') and 'password' in ep_args: - from simplecrypt import decrypt import base64 - crypter, password = base64.decodestring(ep_args.pop('password')).split(':') - ep_args['pwd'] = decrypt(Client.to_url(ep_args), base64.decodestring(password)) + crypter, password = base64.b64decode(ep_args.pop('password').encode('utf8')).split(b':') + if crypter == 'simplecrypt': + import simplecrypt + ep_args['pwd'] = simplecrypt.decrypt(Client.to_url(ep_args), base64.b64decode(password)) + elif crypter == 'plain': + ep_args['pwd'] = password.decode('utf-8') else: - ep_args['pwd'] = getpass('Password: ') + ep_args['pwd'] = getpass('Password: ') # pragma: no cover db = Client(**ep_args) self.add_db(db) @@ -268,12 +266,12 @@ def connect(self, host=None, dbname=None, user=None, pwd=None, port=8069, protoc :param bool no_save: if set to True database will not be saved to session :return: Client object """ - if interactive: + if interactive: # pragma: no cover # ask user for connection data if not provided, if interactive set # to True - host = host or raw_input('Server Host: ') - dbname = dbname or raw_input('Database name: ') - user = user or raw_input('ERP Login: ') + host = host or xinput('Server Host: ') + dbname = dbname or xinput('Database name: ') + user = user or xinput('Login: ') pwd = pwd or getpass("Password: ") url = Client.to_url(inst=None, @@ -288,17 +286,16 @@ def connect(self, host=None, dbname=None, user=None, pwd=None, port=8069, protoc return db db = Client(host=host, dbname=dbname, user=user, pwd=pwd, port=port, protocol=protocol) - self._add_db(url, db) - db._no_save = no_save # disalows saving database connection in session + self.add_db(db) + db._no_save = no_save # if set to True, disalows saving database connection in session return db def _get_db_init_args(self, database): if isinstance(database, Client): res = database.get_init_args() if self.option('store_passwords') and database._pwd: - from simplecrypt import encrypt import base64 - password = base64.encodestring('simplecrypt:' + base64.encodestring(encrypt(database.get_url(), database._pwd))) + password = base64.b64encode(b'plain:' + database._pwd.encode('utf-8')).decode('utf-8') res.update({'password': password}) return res elif isinstance(database, dict): @@ -310,21 +307,18 @@ def save(self): """ Saves session on disc """ databases = {} - for url, database in self._databases.iteritems(): + for url, database in self._databases.items(): if not getattr(database, '_no_save', False): init_args = self._get_db_init_args(database) databases[url] = init_args data = { 'databases': databases, - 'extra_paths': list(self._extra_paths), 'aliases': self._db_aliases, - 'start_up_imports': self._start_up_imports, 'options': self._options, } - with open(self.data_file, 'wt') as json_data: - json.dump(data, json_data, indent=4) + json_write(self.data_file, data, indent=4) # Overridden to be able to access database like # session[url_or_index] @@ -332,7 +326,7 @@ def __getitem__(self, url_or_index): try: res = self.get_db(url_or_index) except ValueError as e: - raise KeyError(e.message) + raise KeyError(str(e)) return res # Overriden to be able to access database like @@ -341,52 +335,13 @@ def __getattr__(self, name): try: res = self.get_db(name) except ValueError as e: - raise AttributeError(e.message) + raise AttributeError(str(e)) return res def __str__(self): return pprint.pformat(self.index) def __dir__(self): - res = dir(super(ERP_Session, self)) + res = dir(super(Session, self)) res += self.aliases.keys() return res - - -# For Backward compatability -ERP_Session = Session - - -# TODO: move to repr / ipython extension -class IPYSession(Session): - def _repr_html_(self): - """ Provides HTML representation of session (Used for IPython) - """ - from openerp_proxy.utils import ustr as _ - - def _get_data(): - for url in self._databases.keys(): - index = self._index_url(url) - aliases = (_(al) for al, aurl in self.aliases.items() if aurl == url) - yield (url, index, u", ".join(aliases)) - ttable = u"%s
                                  " - trow = u"%s" - tdata = u"%s" - caption = u"Previous connections" - hrow = u"DB URLDB IndexDB Aliases" - help_text = (u"
                                  " - u"To get connection just call
                                    " - u"
                                  • session.aliase
                                  • " - u"
                                  • session[index]
                                  • " - u"
                                  • session[aliase]
                                  • " - u"
                                  • session[url]
                                  • " - u"
                                  • session.get_db(url|index|aliase)
                                  • " - u"
                                  ") - - data = u"" - for row in _get_data(): - data += trow % (u''.join((tdata % i for i in row))) - - table = ttable % (caption + hrow + data) - - return u"
                                  %s %s
                                  " % (table, help_text) diff --git a/openerp_proxy/tests/__init__.py b/openerp_proxy/tests/__init__.py index 91a3313..7be27ad 100644 --- a/openerp_proxy/tests/__init__.py +++ b/openerp_proxy/tests/__init__.py @@ -1,3 +1,4 @@ +import six import unittest from openerp_proxy.utils import AttrDict @@ -20,3 +21,12 @@ def setUp(self): 'super_password': os.environ.get('ODOO_TEST_SUPER_PASSWORD', 'admin'), }) + # allow to specify if extensions should be enabled for testing + self.with_extensions = os.environ.get('TEST_WITH_EXTENSIONS', False) + if self.with_extensions: + import openerp_proxy.ext.all + + if six.PY3: + def assertItemsEqual(self, *args, **kwargs): + return self.assertCountEqual(*args, **kwargs) + diff --git a/openerp_proxy/tests/all.py b/openerp_proxy/tests/all.py index a564f1f..55688d5 100644 --- a/openerp_proxy/tests/all.py +++ b/openerp_proxy/tests/all.py @@ -1,2 +1,7 @@ from .test_connection import * from .test_client import * +from .test_orm import * +from .test_plugins import * +from .test_session import * +from .ext.test_sugar import * +from .ext.test_workflow import * diff --git a/openerp_proxy/tests/ext/__init__.py b/openerp_proxy/tests/ext/__init__.py new file mode 100644 index 0000000..cc17931 --- /dev/null +++ b/openerp_proxy/tests/ext/__init__.py @@ -0,0 +1,10 @@ +##import six +#import unittest +#import os +#from openerp_proxy.tests.test_sugar import * +##from openerp_proxy.tests import BaseTestCase + + +##@unittest.skipUnless(os.environ.get('TEST_WITH_EXTENSIONS', False), 'requires tests enabled') +##class BaseExtTestCase(BaseTestCase): + ##pass diff --git a/openerp_proxy/tests/ext/test_sugar.py b/openerp_proxy/tests/ext/test_sugar.py new file mode 100644 index 0000000..ba6f988 --- /dev/null +++ b/openerp_proxy/tests/ext/test_sugar.py @@ -0,0 +1,79 @@ +import unittest +import os + +try: + import unittest.mock as mock +except ImportError: + import mock + +from openerp_proxy.tests import BaseTestCase +from openerp_proxy.core import Client +from openerp_proxy.orm.record import (Record, + RecordList) +from openerp_proxy.orm.object import Object + + +@unittest.skipUnless(os.environ.get('TEST_WITH_EXTENSIONS', False), 'requires extensions enabled') +class Test_31_ExtSugar(BaseTestCase): + def setUp(self): + super(Test_31_ExtSugar, self).setUp() + + self.client = Client(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port) + self.object = self.client.get_obj('res.partner') + self.record = self.object.browse(1) + self.obj_ids = self.object.search([], limit=10) + self.recordlist = self.object.read_records(self.obj_ids) + + def test_obj_search_record(self): + res = self.object.search_record([('name', 'ilike', 'admin')]) + self.assertIsInstance(res, Record) + self.assertEqual(res.name, 'Administrator') + + def test_obj_getitem(self): + res = self.object[self.record.id] + self.assertIsInstance(res, Record) + self.assertEqual(res, self.record) + + with self.assertRaises(KeyError): + self.object['bad key'] + + def test_obj_len(self): + self.assertEqual(len(self.object), self.object.search([], count=True)) + + def test_obj_call_name_search(self): + res = self.object('admin') # name_search by name. only one record with this name + self.assertIsInstance(res, Record) + self.assertEqual(res._name, 'Administrator') + + res = self.object('Bank') + self.assertIsInstance(res, RecordList) + bank_ids = [i for i, _ in self.object.name_search('Bank')] + self.assertItemsEqual(res.ids, bank_ids) + + def test_obj_call_search_records(self): + with mock.patch.object(self.object, 'search_records') as fake_search_records: + self.object([('name', 'ilike', 'admin')]) + fake_search_records.assert_called_with([('name', 'ilike', 'admin')]) + + self.object([('name', 'ilike', 'admin')], count=True) + fake_search_records.assert_called_with([('name', 'ilike', 'admin')], count=True) + + self.object(name='admin') + fake_search_records.assert_called_with([('name', '=', 'admin')]) + + def test_client_dir(self): + self.assertIn('_res_partner', dir(self.client)) + + def test_client_getattr(self): + res = self.client._res_partner + self.assertIsInstance(res, Object) + self.assertEqual(res, self.object) + + with self.assertRaises(AttributeError): + self.client._some_bad_model + diff --git a/openerp_proxy/tests/ext/test_workflow.py b/openerp_proxy/tests/ext/test_workflow.py new file mode 100644 index 0000000..c956b66 --- /dev/null +++ b/openerp_proxy/tests/ext/test_workflow.py @@ -0,0 +1,70 @@ +import unittest +import os + +try: + import unittest.mock as mock +except ImportError: + import mock + +from openerp_proxy.tests import BaseTestCase +from openerp_proxy.core import Client +from openerp_proxy.orm.record import (Record, + RecordList) +from openerp_proxy.orm.object import Object + + +@unittest.skipUnless(os.environ.get('TEST_WITH_EXTENSIONS', False), 'requires tests enabled') +class Test_32_ExtWorkFlow(BaseTestCase): + def setUp(self): + super(Test_32_ExtWorkFlow, self).setUp() + + self.client = Client(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port) + self.object = self.client.get_obj('sale.order') + self.record = self.object.browse(1) + self.obj_ids = self.object.search([], limit=10) + self.recordlist = self.object.read_records(self.obj_ids) + + def test_obj_workflow(self): + res = self.object.workflow + self.assertIsInstance(res, Record) + self.assertEqual(res._object.name, 'workflow') + self.assertEqual(res.osv, 'sale.order') + + def test_record_wkf_instance(self): + res = self.record.workflow_instance + self.assertIsInstance(res, Record) + self.assertEqual(res.wkf_id.id, self.object.workflow.id) + self.assertEqual(res.res_id, self.record.id) + + def test_record_wkf_items(self): + res = self.record.workflow_items + self.assertIsInstance(res, RecordList) + self.assertEqual(len(res), 1) + self.assertEqual(res[0]._object.name, 'workflow.workitem') + + @unittest.skipIf(os.environ.get('TEST_WITHOUT_DB_CHANGES', False), 'db changes not allowed. skipped') + def test_record_signal_send(self): + # first sale order seems to be in draft state on just created DB + so = self.record + + # get current SO activity + act = so.workflow_items[0].act_id + act_id = act.id + + # get first avalable transition with signal + trans = [t for t in act.out_transitions if t.signal] + if not trans: + raise unittest.SkipTest("There is no avalable transitions in first sale order to test workflow") + trans = trans[0] + + # send signal + so.workflow_signal(trans.signal) + so.refresh() # refresh record to reflect database changes + + # test it + self.assertNotEqual(so.workflow_items[0].act_id.id, act_id) diff --git a/openerp_proxy/tests/test_client.py b/openerp_proxy/tests/test_client.py index aeaf1f1..3fab1b4 100644 --- a/openerp_proxy/tests/test_client.py +++ b/openerp_proxy/tests/test_client.py @@ -1,9 +1,11 @@ +from pkg_resources import parse_version + from . import BaseTestCase from openerp_proxy.core import Client from openerp_proxy.orm.object import Object from openerp_proxy.orm.record import Record +from openerp_proxy.service.service import ServiceManager from openerp_proxy.plugin import Plugin -from openerp_proxy.exceptions import LoginException class Test_10_Client(BaseTestCase): @@ -22,6 +24,12 @@ def test_20_username(self): self.assertIsInstance(self.client.user, Record) self.assertEqual(self.client.user.login, self.env.user) + def test_25_server_version(self): + # Check that server version is wrapped in parse_version. thi allows to + # compare versions + self.assertIsInstance(self.client.server_version, type(parse_version('1.0.0'))) + + def test_30_get_obj(self): self.assertIn('res.partner', self.client.registered_objects) obj = self.client.get_obj('res.partner') @@ -46,6 +54,15 @@ def test_50_to_url(self): self.assertEqual(Client.to_url(None, **self.env), cl_url) self.assertEqual(self.client.get_url(), cl_url) + with self.assertRaises(ValueError): + Client.to_url('strange thing') + + def test_55_str(self): + self.assertEqual(str(self.client), u"Client: %s" % self.client.get_url()) + + def test_55_repr(self): + self.assertEqual(repr(self.client), str(self.client)) + def test_60_plugins(self): self.assertIn('Test', self.client.plugins.registered_plugins) self.assertIn('Test', self.client.plugins) @@ -56,6 +73,7 @@ def test_60_plugins(self): # check plugin's method result self.assertEqual(self.client.get_url(), self.client.plugins.Test.test()) + self.assertEqual(repr(self.client.plugins.Test), 'openerp_proxy.plugin.Plugin:Test') def test_62_plugins_wrong_name(self): self.assertNotIn('Test_Bad', self.client.plugins.registered_plugins) @@ -67,3 +85,28 @@ def test_62_plugins_wrong_name(self): with self.assertRaises(AttributeError): self.client.plugins.Test_Bad + + def test_70_client_services(self): + self.assertIsInstance(self.client.services, ServiceManager) + self.assertIn('db', self.client.services) + self.assertIn('object', self.client.services) + self.assertIn('report', self.client.services) + + self.assertIn('db', self.client.services.list) + self.assertIn('object', self.client.services.list) + self.assertIn('report', self.client.services.list) + + self.assertIn('db', dir(self.client.services)) + self.assertIn('object', dir(self.client.services)) + self.assertIn('report', dir(self.client.services)) + + def test_80_execute(self): + res = self.client.execute('res.partner', 'read', 1) + self.assertIsInstance(res, dict) + self.assertEqual(res['id'], 1) + + res = self.client.execute('res.partner', 'read', [1]) + self.assertIsInstance(res, list) + self.assertEqual(len(res), 1) + self.assertIsInstance(res[0], dict) + self.assertEqual(res[0]['id'], 1) diff --git a/openerp_proxy/tests/test_orm.py b/openerp_proxy/tests/test_orm.py new file mode 100644 index 0000000..cd9b7a8 --- /dev/null +++ b/openerp_proxy/tests/test_orm.py @@ -0,0 +1,607 @@ +from . import BaseTestCase +from openerp_proxy.core import Client +from openerp_proxy.orm.record import (Record, + RecordList, + get_record_list) +from openerp_proxy.exceptions import ConnectorError + +try: + import unittest.mock as mock +except ImportError: + import mock + +import numbers +import collections + + +class Test_20_Object(BaseTestCase): + + def setUp(self): + super(self.__class__, self).setUp() + self.client = Client(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port) + self.object = self.client.get_obj('res.partner') + + def test_dir(self): + self.assertIn('read', dir(self.object)) + self.assertIn('search', dir(self.object)) + self.assertIn('write', dir(self.object)) + self.assertIn('unlink', dir(self.object)) + self.assertIn('create', dir(self.object)) + + # test if normal mehtods avalilable in dir(object) + #self.assertIn('search_records', dir(self.object)) + #self.assertIn('browse', dir(self.object)) + + def test_getttr(self): + self.assertEqual(self.object.search.__name__, 'res.partner:search') + + # Test that attibute error is raised on access on private methods + with self.assertRaises(AttributeError): + self.object._do_smthing_private + + def test_call_unexistent_method(self): + # method wrapper will be created + self.assertEqual(self.object.some_unexisting_mehtod.__name__, 'res.partner:some_unexisting_mehtod') + + # but exception should be raised + with self.assertRaises(ConnectorError): + self.object.some_unexisting_mehtod([1]) + + def test_model(self): + self.assertIsInstance(self.object.model, Record) + self.assertEqual(self.object.name, self.object.model.model) + self.assertEqual(self.object.model, self.object._model) + + # this will check that model_name is result of name_get on model record + self.assertEqual(self.object.model_name, self.object.model._name) + + def test_search(self): + res = self.object.search([('id', '=', 1)]) + self.assertIsInstance(res, list) + self.assertEqual(res, [1]) + + res = self.object.search([('id', '=', 1)], count=1) + self.assertIsInstance(res, numbers.Integral) + self.assertEqual(res, 1) + + def test_search_records(self): + res = self.object.search_records([('id', '=', 1)]) + self.assertIsInstance(res, RecordList) + self.assertEqual(res.length, 1) + self.assertEqual(res[0].id, 1) + + res = self.object.search_records([('id', '=', 99999)]) + self.assertIsInstance(res, RecordList) + self.assertEqual(res.length, 0) + + res = self.object.search_records([('id', '=', 1)], count=1) + self.assertIsInstance(res, numbers.Integral) + self.assertEqual(res, 1) + + # test search_records with read_fields argument + res = self.object.search_records([], read_fields=['name', 'country_id'], limit=10) + self.assertIsInstance(res, RecordList) + self.assertEqual(res.length, 10) + self.assertEqual(len(res._lcache), res.length) + for record in res: + self.assertEqual(len(record._data), 3) + self.assertItemsEqual(list(record._data), ['id', 'name', 'country_id']) + + def test_read_records(self): + # read one record + res = self.object.read_records(1) + self.assertIsInstance(res, Record) + self.assertEqual(res.id, 1) + + # read set of records + res = self.object.read_records([1]) + self.assertIsInstance(res, RecordList) + self.assertEqual(res.length, 1) + self.assertEqual(res[0].id, 1) + + # try to call read_records with bad argument + with self.assertRaises(ValueError): + self.object.read_records(None) + + # Test read with specified fields + record = self.object.read_records(1, ['name', 'country_id']) + self.assertEqual(len(record._data), 3) + self.assertItemsEqual(list(record._data), ['id', 'name', 'country_id']) + + def test_browse(self): + with mock.patch.object(self.object, 'read_records') as fake_read_records: + self.object.browse(1) + fake_read_records.assert_called_with(1) + + with mock.patch.object(self.object, 'read_records') as fake_read_records: + self.object.browse([1]) + fake_read_records.assert_called_with([1]) + + with mock.patch.object(self.object, 'read_records') as fake_read_records: + self.object.browse(None) + fake_read_records.assert_called_with(None) + + def test_object_equal(self): + self.assertEqual(self.object, self.client['res.partner']) + self.assertIs(self.object, self.client['res.partner']) + self.assertNotEqual(self.object, self.client['res.users']) + self.assertIsNot(self.object, self.client['res.users']) + + def test_str(self): + self.assertEqual(str(self.object), u"Object ('res.partner')") + + def test_repr(self): + self.assertEqual(repr(self.object), str(self.object)) + + +class Test_21_Record(BaseTestCase): + + def setUp(self): + super(self.__class__, self).setUp() + self.client = Client(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port) + self.object = self.client.get_obj('res.partner') + self.record = self.object.browse(1) + + def test_dir(self): + self.assertIn('read', dir(self.record)) + self.assertIn('search', dir(self.record)) + self.assertIn('write', dir(self.record)) + self.assertIn('unlink', dir(self.record)) + + # test if normal mehtods avalilable in dir(object) + #self.assertIn('refresh', dir(self.record)) + + def test_proxy_property(self): + self.assertIs(self.record._proxy, self.client) + self.assertIs(self.record._object.proxy, self.client) + self.assertIs(self.object.proxy, self.client) + + def test_as_dict(self): + rdict = self.record.as_dict + + self.assertIsInstance(rdict, dict) + self.assertIsNot(rdict, self.record._data) + self.assertItemsEqual(rdict, self.record._data) + + # test that changes to rdict will not calue changes to record's data + rdict['new_key'] = 'new value' + + self.assertIn('new_key', rdict) + self.assertNotIn('new_key', self.record._data) + + def test_str(self): + self.assertEqual(str(self.record), u"R(res.partner, 1)[%s]" % (self.record.name_get()[0][1])) + + def test_repr(self): + self.assertEqual(str(self.record), repr(self.record)) + + def test_name_get(self): + self.assertEqual(self.record._name, self.record.name_get()[0][1]) + + def test_record_equal(self): + rec1 = self.record + + rec_list = self.object.search_records([('id', '=', 1)]) + self.assertIsInstance(rec_list, RecordList) + self.assertEqual(rec_list.length, 1) + + rec2 = rec_list[0] + self.assertEqual(rec1, rec2) + self.assertEqual(rec1.id, rec2.id) + self.assertEqual(rec1._name, rec2._name) + + # Test that equality with simple integers works + self.assertEqual(rec1, rec2.id) + self.assertEqual(rec1.id, rec2) + + self.assertNotEqual(rec1, None) + self.assertNotEqual(rec1, 2) + + def test_getitem(self): + self.assertEqual(self.record['id'], self.record.id) + with self.assertRaises(KeyError): + self.record['some_unexistent_field'] + + def test_getattr(self): + # Check that, if we try to get unexistent field, result will be method + # wrapper for object method + f = self.record.some_unexistent_field + self.assertTrue(callable(f)) + + def test_record_to_int(self): + self.assertIs(int(self.record), 1) + + def test_record_hash(self): + self.assertEqual(hash(self.record), hash((self.record._object.name, self.record.id))) + + def test_record_relational_fields(self): + res = self.record.child_ids # read data from res.partner:child_ids field + + self.assertIsInstance(res, RecordList) + self.assertTrue(res.length >= 1) + self.assertIsInstance(res[0], Record) + self.assertEqual(res[0]._object.name, 'res.partner') + + # test many2one + self.assertIsInstance(res[0].parent_id, Record) + self.assertIsNot(res[0].parent_id, self.record) + self.assertEqual(res[0].parent_id, self.record) + + # test that empty many2one field is avaluated as False + self.assertIs(self.record.user_id, False) + + # test that empty x2many field is evaluated as empty RecordList + self.assertIsInstance(self.record.user_ids, RecordList) + self.assertEqual(self.record.user_ids.length, 0) + + def test_record_refresh(self): + # read all data for record + self.record.read() + + # read company_id field + self.record.company_id.name + + # check that data had been loaded + self.assertTrue(len(self.record._data.keys()) > 5) + + # test before refresh + self.assertEqual(len(self.record._cache.keys()), 2) + self.assertIn('res.partner', self.record._cache) + self.assertIn('res.company', self.record._cache) + self.assertIn(len(list(self.record._cache['res.company'].values())[0]), [2, 3]) + self.assertIn('name', list(self.record._cache['res.company'].values())[0]) + + # refresh record + self.record.refresh() + + # test after refresh + self.assertEqual(len(self.record._data.keys()), 1) + self.assertItemsEqual(list(self.record._data), ['id']) + self.assertEqual(len(self.record._cache.keys()), 2) + self.assertIn('res.partner', self.record._cache) + self.assertIn('res.company', self.record._cache) + self.assertEqual(len(list(self.record._cache['res.company'].values())[0]), 1) + self.assertNotIn('name', list(self.record._cache['res.company'].values())[0]) + + +class Test_22_RecordList(BaseTestCase): + + def setUp(self): + super(self.__class__, self).setUp() + self.client = Client(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port) + self.object = self.client.get_obj('res.partner') + self.obj_ids = self.object.search([], limit=10) + self.recordlist = self.object.read_records(self.obj_ids) + + def test_ids(self): + self.assertSequenceEqual(self.recordlist.ids, self.obj_ids) + + def test_length(self): + self.assertEqual(self.recordlist.length, len(self.obj_ids)) + self.assertEqual(len(self.recordlist), len(self.obj_ids)) + + def test_recods(self): + self.assertIsInstance(self.recordlist.records, list) + self.assertIsInstance(self.recordlist.records[0], Record) + + def test_str(self): + self.assertEqual(str(self.recordlist), u"RecordList(res.partner): length=10") + + def test_repr(self): + self.assertEqual(repr(self.recordlist), str(self.recordlist)) + + def test_getitem(self): + id1 = self.obj_ids[0] + id2 = self.obj_ids[-1] + + id_slice = self.obj_ids[2:15:2] + + self.assertIsInstance(self.recordlist[0], Record) + self.assertEqual(self.recordlist[0].id, id1) + + self.assertIsInstance(self.recordlist[-1], Record) + self.assertEqual(self.recordlist[-1].id, id2) + + res = self.recordlist[2:15:2] + self.assertIsInstance(res, RecordList) + self.assertEqual(res.length, len(id_slice)) + self.assertSequenceEqual(res.ids, id_slice) + + with self.assertRaises(IndexError): + self.recordlist[100] + + def test_getattr(self): + with mock.patch.object(self.object, 'some_server_method') as fake_method: + # bug override in mock object (python 2.7) + if not getattr(fake_method, '__name__', False): + fake_method.__name__ = fake_method.name + self.recordlist.some_server_method('arg1', 'arg2') + fake_method.assert_called_with(self.recordlist.ids, 'arg1', 'arg2') + + def test_delitem(self): + r = self.recordlist[5] + self.assertEqual(len(self.recordlist), 10) + + del self.recordlist[5] + + self.assertEqual(len(self.recordlist), 9) + self.assertNotIn(r, self.recordlist) + + def test_setitem(self): + rec = self.object.search_records([('id', 'not in', self.recordlist.ids)], limit=1)[0] + + old_rec = self.recordlist[8] + + self.assertEqual(len(self.recordlist), 10) + self.assertNotIn(rec, self.recordlist) + self.assertIn(old_rec, self.recordlist) + + self.recordlist[8] = rec + + self.assertEqual(len(self.recordlist), 10) + self.assertIn(rec, self.recordlist) + self.assertNotIn(old_rec, self.recordlist) + + with self.assertRaises(ValueError): + self.recordlist[5] = 25 + + def test_contains(self): + rid = self.obj_ids[0] + rec = self.object.read_records(rid) + + brid = self.object.search([('id', 'not in', self.obj_ids)], limit=1)[0] + brec = self.object.read_records(brid) + + self.assertIn(rid, self.recordlist) + self.assertIn(rec, self.recordlist) + + self.assertNotIn(brid, self.recordlist) + self.assertNotIn(brec, self.recordlist) + + self.assertNotIn(None, self.recordlist) + + def test_insert_record(self): + rec = self.object.search_records([('id', 'not in', self.recordlist.ids)], limit=1)[0] + + self.assertEqual(len(self.recordlist), 10) + self.assertNotIn(rec, self.recordlist) + + self.recordlist.insert(1, rec) + + self.assertEqual(len(self.recordlist), 11) + self.assertIn(rec, self.recordlist) + self.assertEqual(self.recordlist[1], rec) + + def test_insert_by_id(self): + rec = self.object.search_records([('id', 'not in', self.recordlist.ids)], limit=1)[0] + + self.assertEqual(len(self.recordlist), 10) + self.assertNotIn(rec, self.recordlist) + + self.recordlist.insert(1, rec.id) + + self.assertEqual(len(self.recordlist), 11) + self.assertIn(rec, self.recordlist) + self.assertEqual(self.recordlist[1], rec) + + def test_insert_bad_value(self): + rec = self.object.search_records([('id', 'not in', self.recordlist.ids)], limit=1)[0] + + self.assertEqual(len(self.recordlist), 10) + self.assertNotIn(rec, self.recordlist) + + with self.assertRaises(AssertionError): + self.recordlist.insert(1, "some strange type") + + def test_prefetch(self): + cache = self.recordlist._cache + lcache = self.recordlist._lcache + + # check that cache is only filled with ids + self.assertEqual(len(lcache), self.recordlist.length) + for record in self.recordlist: + # Note that record._data is a property, which means + # record._lcache[record.id]. _data property is dictionary. + self.assertEqual(len(record._data), 1) + self.assertItemsEqual(list(record._data), ['id']) + + # prefetch normal field + self.recordlist.prefetch('name') + + self.assertEqual(len(self.recordlist._lcache), self.recordlist.length) + for record in self.recordlist: + self.assertEqual(len(record._data), 2) + self.assertItemsEqual(list(record._data), ['id', 'name']) + + # check that cache contains only res.partner object cache + self.assertEqual(len(cache), 1) + self.assertIn('res.partner', cache) + self.assertNotIn('res.country', cache) + + # prefetch related field name of caountry and country code + self.recordlist.prefetch('country_id.name', 'country_id.code') + + # test that cache now contains two objects ('res.partner', + # 'res.country') + self.assertEqual(len(cache), 2) + self.assertIn('res.partner', cache) + self.assertIn('res.country', cache) + + c_cache = cache['res.country'] + country_checked = False # if in some cases selected partners have no related countries, raise error + for record in self.recordlist: + # test that country_id field was added to partner's cache + self.assertEqual(len(record._data), 3) + self.assertItemsEqual(list(record._data), ['id', 'name', 'country_id']) + + # if current partner have related country_id + # + # Note, here check 'country_id' via '_data' property to avoid lazy + # loading of data. + country_id = record._data['country_id'] + + # if data is in form [id, ] + if isinstance(country_id, collections.Iterable): + country_id = country_id[0] + country_is_list = True + + if country_id: + country_checked = True + + # test, that there are some data for this country_id in country + # cache + self.assertIn(country_id, c_cache) + + # Note that, in case, of related many2one fields, Odoo may + # return list, with Id and resutlt of name_get method. + # thus, we program will imediatly cache this value + if country_is_list: + self.assertEqual(len(c_cache[country_id]), 4) + self.assertItemsEqual(list(c_cache[country_id]), ['id', 'name', 'code', '__name_get_result']) + else: + self.assertEqual(len(c_cache[country_id]), 4) + self.assertItemsEqual(list(c_cache[country_id]), ['id', 'name', 'code', '__name_get_result']) + + self.assertTrue(country_checked, "Country must be checked. may be there are wrong data in test database") + + def test_sorting(self): + def to_names(rlist): + return [r.name for r in rlist] + + names = to_names(self.recordlist) + + self.assertSequenceEqual(sorted(names), to_names(sorted(self.recordlist, key=lambda x: x.name))) + self.assertSequenceEqual(sorted(names, reverse=True), to_names(sorted(self.recordlist, key=lambda x: x.name, reverse=True))) + self.assertSequenceEqual(list(reversed(names)), to_names(reversed(self.recordlist))) + + # test recordlist sort methods + rlist = self.recordlist.copy() + rnames = names[:] # copy list + rlist.sort(key=lambda x: x.name) # inplace sort + rnames.sort() # inplace sort + self.assertSequenceEqual(rnames, to_names(rlist)) + + # test recordlist reverse method + rlist = self.recordlist.copy() + rnames = names[:] # copy list + rlist.reverse() # inplace reverse + rnames.reverse() # inplace reverse + self.assertSequenceEqual(rnames, to_names(rlist)) + + def test_search(self): + # TODO: test for context + with mock.patch.object(self.object, 'search') as fake_method: + self.recordlist.search([('id', '!=', 1)], limit=5) + fake_method.assert_called_with([('id', 'in', self.recordlist.ids), ('id', '!=', 1)], limit=5) + + def test_search_records(self): + # TODO: test for context + with mock.patch.object(self.object, 'search_records') as fake_method: + self.recordlist.search_records([('id', '!=', 1)], limit=4) + fake_method.assert_called_with([('id', 'in', self.recordlist.ids), ('id', '!=', 1)], limit=4) + + def test_read(self): + # TODO: test for context + # or remove this test and method, because getattr pass context + # too + with mock.patch.object(self.object, 'read') as fake_method: + self.recordlist.read(['name']) + fake_method.assert_called_with(self.recordlist.ids, ['name']) + + def test_filter(self): + res = self.recordlist.filter(lambda x: x.id % 2 == 0) + expected_ids = [r.id for r in self.recordlist if r.id % 2 == 0] + self.assertIsInstance(res, RecordList) + self.assertEqual(res.ids, expected_ids) + + def test_group_by(self): + res = self.recordlist.group_by(lambda x: x.id % 2 == 0) + self.assertIsInstance(res, collections.defaultdict) + self.assertItemsEqual(res.keys(), [True, False]) + # TODO: write better test + + res = self.recordlist.group_by('country_id') + + def test_existing(self): + # all existing object ids + all_obj_ids = self.object.search([], limit=False) + + # generate 10 unexisting ids + unexistent_ids = list(range(max(all_obj_ids) + 1, max(all_obj_ids) + 40, 4)) + self.assertEqual(len(unexistent_ids), 10) + + # test simple existense + rlist = get_record_list(self.object, all_obj_ids[:10] + unexistent_ids) + self.assertEqual(len(rlist), 20) + elist = rlist.existing() + self.assertEqual(len(elist), 10) + self.assertItemsEqual(elist.ids, all_obj_ids[:10]) + + # test existense with repeated items + rlist = get_record_list(self.object, all_obj_ids[:10] + unexistent_ids + all_obj_ids[:5]) + self.assertEqual(len(rlist), 25) + + # with uniqify=True (defualt) + elist = rlist.existing() + self.assertEqual(len(elist), 10) + self.assertItemsEqual(elist.ids, all_obj_ids[:10]) + + # with uniqify=False + elist = rlist.existing(uniqify=False) + self.assertEqual(len(elist), 15) + self.assertItemsEqual(elist.ids, all_obj_ids[:10] + all_obj_ids[:5]) + + def test_refresh(self): + # save cache pointers to local namespase to simplify access to it + cache = self.recordlist._cache + pcache = cache['res.partner'] # partner cache + ccache = cache['res.country'] # country cache + + # load data to record list + self.recordlist.prefetch('name', 'country_id.name', 'country_id.code') + + # create related records. This is still required, becuase, prefetch + # just fills cache without creating record instances + for rec in self.recordlist: + rec.country_id + + self.assertTrue(len(pcache) == len(self.recordlist)) + self.assertTrue(len(ccache) > 2) + + clen = len(ccache) + + for data in pcache.values(): + self.assertItemsEqual(list(data), ['id', 'name', 'country_id']) + + for data in ccache.values(): + if '__name_get_result' in data: + self.assertItemsEqual(list(data), ['id', 'name', 'code', '__name_get_result']) + else: + self.assertItemsEqual(list(data), ['id', 'name', 'code']) + + # refresh recordlist + self.recordlist.refresh() + + self.assertTrue(len(pcache) == len(self.recordlist)) + self.assertTrue(len(ccache) == clen) + + for data in pcache.values(): + self.assertItemsEqual(list(data), ['id']) + + for data in ccache.values(): + self.assertItemsEqual(list(data), ['id']) diff --git a/openerp_proxy/tests/test_plugins.py b/openerp_proxy/tests/test_plugins.py new file mode 100644 index 0000000..6866caa --- /dev/null +++ b/openerp_proxy/tests/test_plugins.py @@ -0,0 +1,76 @@ +from . import BaseTestCase +from openerp_proxy.core import Client +from openerp_proxy.orm.record import Record +from openerp_proxy.orm.record import RecordList +from openerp_proxy.exceptions import ConnectorError + +import unittest + +try: + import unittest.mock as mock +except ImportError: + import mock + +import numbers +import collections + + +class Test_25_Plugin_ModuleUtils(BaseTestCase): + + def setUp(self): + super(self.__class__, self).setUp() + self.client = Client(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port) + + def test_10_init_module_utils(self): + self.assertNotIn('module_utils', self.client.plugins) + + import openerp_proxy.plugins.module_utils + + self.assertIn('module_utils', self.client.plugins) + + def test_15_modules(self): + self.assertIn('sale', self.client.plugins.module_utils.modules) + + def test_20_modules_dir(self): + self.assertIn('m_sale', dir(self.client.plugins.module_utils)) + + def test_25_module_getitem(self): + res = self.client.plugins.module_utils['sale'] + self.assertIsInstance(res, Record) + self.assertEqual(res._object.name, 'ir.module.module') + + from openerp_proxy.plugins.module_utils import ModuleObject + + self.assertIn(ModuleObject, res._object.__class__.__bases__) + + def test_30_module_getattr(self): + res = self.client.plugins.module_utils.m_sale + + self.assertIsInstance(res, Record) + self.assertEqual(res._object.name, 'ir.module.module') + + from openerp_proxy.plugins.module_utils import ModuleObject + + self.assertIn(ModuleObject, res._object.__class__.__bases__) + + def test_35_module_install(self): + smod = self.client.plugins.module_utils.m_sale + + if smod.state == 'installed': + raise unittest.SkipTest('Module already installed') + + self.assertNotEqual(smod.state, 'installed') + smod.install() + smod.refresh() # reread data from database + self.assertEqual(smod.state, 'installed') + + def test_40_module_upgrade(self): + smod = self.client.plugins.module_utils.m_sale + + self.assertEqual(smod.state, 'installed') + smod.upgrade() # just call it diff --git a/openerp_proxy/tests/test_session.py b/openerp_proxy/tests/test_session.py new file mode 100644 index 0000000..5b26dcf --- /dev/null +++ b/openerp_proxy/tests/test_session.py @@ -0,0 +1,213 @@ +from . import BaseTestCase +from openerp_proxy.core import Client +from openerp_proxy.session import Session + +try: + import unittest.mock as mock +except ImportError: + import mock + +import sys +import os +import os.path + + +class Test_90_Session(BaseTestCase): + + def setUp(self): + super(self.__class__, self).setUp() + self._session_file_path = '/tmp/openerp_proxy.session.json' + + def tearDown(self): + if os.path.exists(self._session_file_path): + os.unlink(self._session_file_path) + + def test_01_init_save(self): + session = Session(self._session_file_path) + self.assertFalse(os.path.exists(self._session_file_path)) + session.save() + self.assertTrue(os.path.exists(self._session_file_path)) + + def test_05_add_path(self): + old_sys_path = sys.path[:] + self.assertNotIn('/new_path', sys.path) + session = Session(self._session_file_path) + session.add_path('/new_path') + self.assertIn('/new_path', sys.path) + session.save() + del session + sys.path = old_sys_path[:] + self.assertNotIn('/new_path', sys.path) + + # test that path is automaticaly added on new session init + session = Session(self._session_file_path) + self.assertIn('/new_path', sys.path) + del session + sys.path = old_sys_path[:] + self.assertNotIn('/new_path', sys.path) + + def test_10_option(self): + session = Session(self._session_file_path) + self.assertIs(session.option('store_passwords'), None) + session.option('store_passwords', True) + self.assertTrue(session.option('store_passwords')) + session.save() + del session + + session = Session(self._session_file_path) + self.assertTrue(session.option('store_passwords')) + + def test_15_connect_save_connect(self): + session = Session(self._session_file_path) + + # set store_passwords to true, to avoid password promt during tests + session.option('store_passwords', True) + + cl = session.connect(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port, + interactive=False) + + self.assertIsInstance(cl, Client) + self.assertIn(cl.get_url(), session._databases) + self.assertIn(cl.get_url(), session.db_list) + self.assertEqual(len(session.db_list), 1) + self.assertIs(session.get_db(cl.get_url()), cl) + self.assertEqual(session.index[1], cl.get_url()) # first db must be with index=1 + + # index and url may be used in this way too + self.assertIs(session[cl.get_url()], cl) + self.assertIs(session[1], cl) + + # save the session + session.save() + del session + + # recreate session + session = Session(self._session_file_path) + + # and test again + self.assertIn(cl.get_url(), session._databases) + self.assertIn(cl.get_url(), session.db_list) + self.assertEqual(len(session.db_list), 1) + self.assertIsNot(session.get_db(cl.get_url()), cl) + self.assertEqual(session.get_db(cl.get_url()), cl) + self.assertEqual(session.index[1], cl.get_url()) # first db must be with index=1 + self.assertIsNot(session[cl.get_url()], cl) + self.assertIsNot(session[1], cl) + self.assertEqual(session[cl.get_url()], cl) + self.assertEqual(session[1], cl) + del session + + # test situation when session just started and saved, without changes + # this code is aimed mostly to increase test coverage. In this case in + # ._databases all values will be dict when saveing + session = Session(self._session_file_path) + session.save() + + def test_20_connect_save_connect_no_save(self): + session = Session(self._session_file_path) + + # set store_passwords to true, to avoid password promt during tests + session.option('store_passwords', True) + + cl = session.connect(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port, + interactive=False, + no_save=True) # this arg is different from previous test + + self.assertIsInstance(cl, Client) + self.assertIn(cl.get_url(), session._databases) + self.assertIn(cl.get_url(), session.db_list) + self.assertEqual(len(session.db_list), 1) + self.assertIs(session.get_db(cl.get_url()), cl) + self.assertEqual(session.index[1], cl.get_url()) # first db must be with index=1 + + # index and url may be used in this way too + self.assertIs(session[cl.get_url()], cl) + self.assertIs(session[1], cl) + + # save the session + session.save() + del session + + # recreate session + session = Session(self._session_file_path) + + # and test again + self.assertNotIn(cl.get_url(), session._databases) + self.assertNotIn(cl.get_url(), session.db_list) + self.assertEqual(len(session.db_list), 0) + + with self.assertRaises(ValueError): + session.get_db(cl.get_url()) + + with self.assertRaises(KeyError): + session[cl.get_url()] + + def test_25_aliases(self): + session = Session(self._session_file_path) + + # set store_passwords to true, to avoid password promt during tests + session.option('store_passwords', True) + + cl = session.connect(self.env.host, + dbname=self.env.dbname, + user=self.env.user, + pwd=self.env.password, + protocol=self.env.protocol, + port=self.env.port, + interactive=False) + + self.assertEqual(len(session.aliases), 0) + + res = session.aliase('cl1', cl) + self.assertIs(res, cl) + + res = session.aliase('cl2', 1) # use index + self.assertIs(res, 1) + + res = session.aliase('cl3', cl.get_url()) # use url + self.assertEqual(res, cl.get_url()) + + with self.assertRaises(ValueError): + session.aliase('cl4', 'bad url') + + self.assertIn('cl1', session.aliases) + self.assertIs(session.get_db('cl1'), cl) + self.assertIs(session['cl1'], cl) + self.assertIs(session.cl1, cl) + + self.assertIs(session.cl1, session.cl2) + self.assertIs(session.cl1, session.cl3) + self.assertIn('cl1', dir(session)) + + # save the session + session.save() + del session + + # recreate session + session = Session(self._session_file_path) + + # and test again + self.assertTrue(bool(session.index)) + self.assertEqual(len(session.aliases), 3) + self.assertIn('cl1', session.aliases) + self.assertEqual(session.get_db('cl1'), cl) + self.assertEqual(session['cl1'], cl) + self.assertEqual(session.cl1, cl) + + self.assertIs(session.cl1, session.cl2) + self.assertIs(session.cl1, session.cl3) + + with self.assertRaises(AttributeError): + session.unexistent_aliase + + self.assertIn('cl1', dir(session)) diff --git a/openerp_proxy/utils.py b/openerp_proxy/utils.py index 578837b..6e4b033 100644 --- a/openerp_proxy/utils.py +++ b/openerp_proxy/utils.py @@ -1,20 +1,36 @@ +import six import sys +import json import functools __all__ = ('ustr', 'AttrDict', 'wpartial') +# Python 2/3 workaround in raw_input +try: + xinput = raw_input +except NameError: + xinput = input -def r_eval(code): - """ Helper function to be used in filters or so - At this moment this function mostly suitable for extensions like - 'openerp_proxy.ext.data' or 'openerp_proxy.ext.repr' + +def json_read(file_path): + """ Read specified json file """ - def r_eval_internal(record): - return eval(code, { - 'r': record, - 'rec': record, - 'record': record}) - return r_eval_internal + with open(file_path, 'rt') as json_data: + data = json.load(json_data) + return data + + +def json_write(file_path, *args, **kwargs): + """ Write data to specified json file + + Note, this function uses dumps function to convert data to json first, + and write only if conversion is successfule. This allows to avoid loss of data + when rewriting file. + """ + json_data = json.dumps(*args, **kwargs) + + with open(file_path, 'wt') as json_file: + json_file.write(json_data) def wpartial(func, *args, **kwargs): @@ -24,13 +40,10 @@ def wpartial(func, *args, **kwargs): """ partial = functools.partial(func, *args, **kwargs) - @functools.wraps(func) - def wrapper(*a, **kw): - return partial(*a, **kw) - return wrapper + return functools.wraps(func)(partial) -# Copied from OpenERP source ustr function +# Copied from Odoo source ustr function def get_encodings(hint_encoding='utf-8'): fallbacks = { 'latin1': 'latin9', @@ -57,12 +70,11 @@ def get_encodings(hint_encoding='utf-8'): def exception_to_unicode(e): - if (sys.version_info[:2] < (2, 6)) and hasattr(e, 'message'): - return ustr(e.message) if hasattr(e, 'args'): return "\n".join((ustr(a) for a in e.args)) + try: - return unicode(e) + return six.text_type(e) except Exception: return u"Unknown message" @@ -88,26 +100,24 @@ def ustr(value, hint_encoding='utf-8', errors='strict'): if isinstance(value, Exception): return exception_to_unicode(value) - if isinstance(value, unicode): + if isinstance(value, six.text_type): return value - if not isinstance(value, basestring): + if not isinstance(value, six.string_types): try: - return unicode(value) + return six.text_type(value) except Exception: raise UnicodeError('unable to convert %r' % (value,)) for ln in get_encodings(hint_encoding): try: - return unicode(value, ln, errors=errors) + return six.text_type(value, ln, errors=errors) except Exception: pass raise UnicodeError('unable to convert %r' % (value,)) class AttrDict(dict): - # TODO: think about reimplementing it via self.__dict__ = self - # (http://stackoverflow.com/questions/4984647/accessing-dict-keys-like-an-attribute-in-python) """ Simple class to make dictionary able to use attribute get operation to get elements it contains using syntax like: diff --git a/openerp_proxy/version.py b/openerp_proxy/version.py index c0ca287..95787d5 100644 --- a/openerp_proxy/version.py +++ b/openerp_proxy/version.py @@ -1 +1 @@ -version = "0.5" +version = "0.5" # pragma: no cover diff --git a/run_tests.bash b/run_tests.bash new file mode 100755 index 0000000..2d60a3b --- /dev/null +++ b/run_tests.bash @@ -0,0 +1,30 @@ +#!/bin/bash + +SCRIPT=`readlink -f "$0"` +# Absolute path this script is in, thus /home/user/bin +SCRIPTPATH=`dirname "$SCRIPT"` + +TEST_MODULE=${TEST_MODULE:-'openerp_proxy.tests.all'}; +PY_VERSIONS=${PY_VERSIONS:-"2.7 3.4"}; + +function test_it { + local py_version=$1; + (cd $SCRIPTPATH && \ + virtualenv venv_test -p python${py_version} && \ + source ./venv_test/bin/activate && \ + pip install --upgrade pip setuptools coverage mock pudb ipython[notebook] simple-crypt && \ + python setup.py develop && \ + rm -f .coverage && \ + coverage run --source openerp_proxy -m unittest -v $TEST_MODULE && \ + coverage html -d coverage && \ + deactivate && \ + rm -rf venv_test) +} + +function main { + for version in $PY_VERSIONS; do + test_it $version; + done +} + +main; diff --git a/run_tests.sh b/run_tests.sh deleted file mode 100755 index 1ddc260..0000000 --- a/run_tests.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -SCRIPT=`readlink -f "$0"` -# Absolute path this script is in, thus /home/user/bin -SCRIPTPATH=`dirname "$SCRIPT"` - -(cd $SCRIPTPATH && rm .coverage && coverage run --source openerp_proxy -m unittest -v openerp_proxy.tests.all && coverage html -d coverage) diff --git a/setup.py b/setup.py index 6bd6549..a86b511 100644 --- a/setup.py +++ b/setup.py @@ -28,15 +28,23 @@ 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: Implementation :: CPython', 'Topic :: Utilities', 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules', ], keywords=['openerp', 'odoo', 'odoo-rpc', 'rpc', 'xmlrpc', 'xml-rpc', 'json-rpc', 'jsonrpc', 'odoo-client', 'ipython'], extras_require={ - 'ipython_shell': ['ipython'], + 'all': ['ipython[all]'], }, install_requires=[ 'six', - 'extend_me>=1.1.2', + 'extend_me>=1.1.3', + 'requests', ], )