Initialization
==============

.. _Requests: https://requests.readthedocs.io/en/latest/
.. _Session: https://requests.readthedocs.io/en/latest/user/advanced/#session-objects

**PyOData** requires an external HTTP library which has API compatible with
Session_ from Requests_.

Get the service
---------------

Basic initialization which is going to work for everybody:

.. code-block:: python

    import pyodata
    import requests

    SERVICE_URL = 'http://services.odata.org/V2/Northwind/Northwind.svc/'

    northwind = pyodata.Client(SERVICE_URL, requests.Session())

Get the service for async libraries
-----------------------------------

Initialization of the session instance is dependent on particular library,
but also must have API compatible Session_ from Requests_.

.. code-block:: python

    import httpx
    import aiohttp

    SERVICE_URL = 'http://services.odata.org/V2/Northwind/Northwind.svc/'
    
    service_httpx   = await pyodata.Client.build_async_client(SERVICE_URL, httpx)
    service_aiohttp = await pyodata.Client.build_async_client(SERVICE_URL, aiohttp.ClientSession())

Get the service proxy client for an OData service requiring sap-client parameter
--------------------------------------------------------------------------------

This is a sample when it is necessary to specify sap-client:

.. code-block:: python

    import pyodata
    import requests

    SERVICE_URL = 'http://services.odata.org/V2/Northwind/Northwind.svc/'

    session = requests.Session()
    param = {'sap-client': '510'}
    session.params = param
    northwind = pyodata.Client(SERVICE_URL, session)

Get the service proxy client for an OData service requiring authentication
--------------------------------------------------------------------------

Let's assume you need to work with a service at
the URL *https://odata.example.com/Secret.svc* and User ID is 'MyUser' with
the password 'MyPassword'.

.. code-block:: python

    import pyodata
    import requests

    SERVICE_URL = 'https://odata.example.com/Secret.svc'

    session = requests.Session()
    session.auth = ('MyUser', 'MyPassword')

    theservice = pyodata.Client(SERVICE_URL, session)

Get the service proxy client for an OData service requiring Certificate authentication
--------------------------------------------------------------------------------------

Let's assume your service requires certificate based authentication and you are
able to export the certificate into the file *mycert.p12*. You need to split
the certificate into public key, private key and certificate authority key.

The following steps has been verified on Fedora Linux and Mac OS.

.. code-block:: bash

    openssl pkcs12 -in mycert.p12 -out ca.pem -cacerts -nokeys
    openssl pkcs12 -in mycert.p12 -out client.pem -clcerts -nokeys
    openssl pkcs12 -in mycert.p12 -out key.pem -nocerts
    openssl rsa -in key.pem -out key_decrypt.pem

You can verify your steps by curl:

.. code-block:: bash

    curl --key key_decrypt.pem --cacert ca.pem --cert client.pem -k 'SERVICE_URL/$metadata'

Python client initialization:

.. code-block:: python

    import pyodata
    import requests

    SERVICE_URL = 'https://odata.example.com/Secret.svc'

    session = requests.Session()
    session.verify = 'ca.pem'
    session.cert = ('client.pem', 'key_decrypt.pem')

    client = pyodata.Client(SERVICE_URL, session)

For more information on client side SSL cerificationcas, please refer to this [gist](https://gist.github.com/mtigas/952344).

Get the service with local metadata
-----------------------------------

It may happen that you will have metadata document for your service downloaded
in a local file and you will want to initialize the service proxy from this
file. In such a case you can provide content of the file as the named argument
`metadata`. Please, make sure you provide `bytes` and not `str` (required by
the xml parser lxml).

.. code-block:: python

    import pyodata
    import requests

    SERVICE_URL = 'http://services.odata.org/V2/Northwind/Northwind.svc/'

    with open('/the/file/path.xml', 'rb') as mtd_file:
        local_metadata = mtd_file.read()

    northwind = pyodata.Client(SERVICE_URL, requests.Session(), metadata=local_metadata)

Dealing with errors during parsing metadata
-------------------------------------------

In the case where you need to consume a service which has not fully valid metadata document and is not under your control, you can configure the metadata parser to try to recover from detected problems.

Parser recovery measures include actions such as using a stub entity type if the parser cannot find a referenced entity type. The stub entity type allows the parser to continue processing the given metadata but causes fatal errors when accessed from the client.

Class config provides easy to use wrapper for all parser configuration. These are:
    - XML namespaces
    - Parser policies (how parser act in case of invalid XML tag). We now support three types of policies:
        - Policy fatal - the policy raises exception and terminates the parser
        - Policy warning - the policy reports the detected problem, executes a fallback code and then continues normally
        - Policy ignore - the policy executes a fallback code without reporting the problem and then continues normally

Parser policies can be specified individually for each XML tag (See enum ParserError for more details). If no policy is specified for the tag, the default policy is used.

For parser to use your custom configuration, it needs to be passed as an argument to the client.

.. code-block:: python

    import pyodata
    from pyodata.v2.model import PolicyFatal, PolicyWarning, PolicyIgnore, ParserError, Config
    import requests

    SERVICE_URL = 'http://services.odata.org/V2/Northwind/Northwind.svc/'

    namespaces = {
        'edmx': 'customEdmxUrl.com',
        'edm': 'customEdmUrl.com'
    }

    custom_config = Config(
        xml_namespaces=namespaces,
        default_error_policy=PolicyFatal(),
        custom_error_policies={
             ParserError.ANNOTATION: PolicyWarning(),
             ParserError.ASSOCIATION: PolicyIgnore()
        })

    northwind = pyodata.Client(SERVICE_URL, requests.Session(), config=custom_config)

Additionally, Schema class has Boolean atribute 'is_valid' that returns if the parser encountered errors. It's value does not depends on used Parser policy. 

.. code-block:: python

    northwind.schema.is_valid

Prevent substitution by default values
--------------------------------------

Per default, missing properties get filled in by type specific default values. While convenient, this throws away
the knowledge of whether a value was missing in the first place.
To prevent this, the class config mentioned in the section above takes an additional parameter, `retain_null`.

.. code-block:: python

    import pyodata
    import requests

    SERVICE_URL = 'http://services.odata.org/V2/Northwind/Northwind.svc/'

    northwind = pyodata.Client(SERVICE_URL, requests.Session(), config=pyodata.v2.model.Config(retain_null=True))

    unknown_shipped_date = northwind.entity_sets.Orders_Qries.get_entity(OrderID=11058,
                                                                         CompanyName='Blauer See Delikatessen').execute()

    print(
        f'Shipped date: {"unknown" if unknown_shipped_date.ShippedDate is None else unknown_shipped_date.ShippedDate}')

Changing `retain_null` to `False` will print `Shipped date: 1753-01-01 00:00:00+00:00`.

Set custom namespaces (Deprecated - use config instead)
-------------------------------------------------------

Let's assume you need to work with a service  which uses namespaces not directly supported by this library e. g. ones
hosted on private urls such as *customEdmxUrl.com* and *customEdmUrl.com*:

.. code-block:: python

    import pyodata
    import requests

    SERVICE_URL = 'http://services.odata.org/V2/Northwind/Northwind.svc/'

    namespaces = {
        'edmx': 'customEdmxUrl.com'
        'edm': 'customEdmUrl.com'
    }

    northwind = pyodata.Client(SERVICE_URL, requests.Session(), namespaces=namespaces)
