Skip to content

Commit

Permalink
Publish 0.1.0 (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
hparfr authored Dec 19, 2016
1 parent fe40f88 commit 11fa49d
Show file tree
Hide file tree
Showing 46 changed files with 1,465 additions and 309 deletions.
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

# 0.1.1

Roadmap / TODO:
- Add Kuehne & Nagel carrier
- Add UPS carrier
- Support test_mode for some carriers
- Improve documentation
- Support additionnal methods of api
- Support carrier tracking
- Write tests

# 0.1.0 2016-12-19

### BREAKING CHANGES
- Laposte API has changed, lot of fields have been renamed
Do `roulier.get('laposte').api()` to get the new fields.
Reason for this change: be constitant with upcoming carriers.
Response of get_label had changed

### Features / Refactorings
- Use cerberus for data validation
- Simplify api
- Add carrier Geodis
- Add carrier DPD France
- Add get_carriers()
- Improve documentation


# 0.0.1 - 2016-09-26

- Publication on pypy https://pypi.python.org/pypi/roulier

# 0.0.0 - 2016

- Add carrier Laposte
- Add carrier Dummy (example)
134 changes: 116 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,155 @@
Roulier
===

Roulier is a shipping library written in Python.
Roulier is a shipping library written in Python for sending parcels.
Roulier will get a label + tracking number to your carrier for you.


Usage :
![big picture](overview.svg)


* Roulier runs on your server and call each carrier API directly.
* You have to use your own credentials provided by each carriers.
* Roulier is Open Source software, AGPL-3
* Roulier integrate a multitude of carriers : Laposte, Geodis, DPD, K&N... more to come.



### Usage

```python
from roulier import roulier

laposte = roulier.get('laposte')

response = laposte.get({
"infos": {
"contractNumber": "12345",
response = laposte.get_label({
"auth": {
"login": "12345",
"password": "password",
},
"service": {
"productCode": "COL"
},
"parcel": {
"insuranceValue": 0,
"nonMachinable": False,
"returnReceipt": False
"weight": 3.4,
},
"receiver_address": {
"country": "DE",
"zip": "93000"
}
}, "getProductInter")
"to_address": {
"firstName": "Hparfr"
"street1": "35 b Rue Montgolfier"
"city": "Villeurbanne"
"country": "FR",
"zip": "69100"
},
"from_address": {
"fristName": "Akretion France"
"street1": "35 b Rue Montgolfier"
"city": "Villeurbanne"
"country": "FR",
"zip": "69100"
},
})


print response

```


To get the full list of parameters:
Get supported carriers:
```python
from roulier import roulier
print roulier.get_carriers()
```

To get the full list of parameters:
```python
from pprint import pprint
from roulier import roulier


laposte = roulier.get('laposte')
obj = laposte.api()
pprint(laposte.api())

obj['infos']['contractNumber'] = '12345'
obj['infos']['password'] = 'password'
obj['service']['productCode'] = 'COL'
# ...

```

Advanced usage for Laposte

Usefull for debugging: get the xml before the call, send an xml directly, analyse the response

```python
from roulier import roulier
laposte = roulier.get('laposte')

#0) create dict for the request as usually
api = laposte.api();
api['auth']['login'] = '12345'
...

# 1) get the sls xml:
req = laposte.encoder.encode(api, 'generateLabelRequest')
# req['body'] contains the xml payload (<sls:generateLabel xmlns:sls="http://sls.ws.coliposte.fr">...</sls:generateLabel>)

# 2) get the soap message
soap_request = laposte.ws.soap_wrap(req['body'], req['headers'])
#soap_request is a string (xml)

# 3) send xml_request to ws
soap_response = laposte.ws.send_request(xml_request)
# soap_response is a Requests response

# 4) interpret the response
data = laposte.ws.handle_response(soap_response)

# 5)get the raw Request Response:
data['response']


```
It's more or less the same for every carrier with SOAP webservice.


Validate input

For input validate we use [Cerberus](http://docs.python-cerberus.org/en/stable/)
```python
from roulier import roulier
laposte = roulier.get('laposte')

# get a ready to fill dict with default values:
laposte.api()


# advanced usage :
from roulier.carriers.laposte.laposte_api import LaposteApi
l_api = LaposteApi()

# get the full schema:
l_api.api_schema()

# validate a dict against the schema
a_dict = { 'auth': {'login': '', 'password': 'password'}, ... }
l_api.errors(a_dict)
# > {'auth': [{'login': ['empty values not allowed']}], ...}

# get a part of schema (like 'parcel')
l_api._parcel()
```


###Contributors


* [@hparfr](https://github.com/hparfr) ([Akretion.com](https://akretion.com))
* [@damdam-s](https://github.com/damdam-s) ([Camp2Camp.com](http://camptocamp.com))
* [@bealdav](https://github.com/bealdav) ([Akretion.com](https://akretion.com))


### Dependencies

* [Cerberus](http://docs.python-cerberus.org/) - input validation and normalization
* [lxml](http://lxml.de/) - XML parsing
* [Jinja2](http://jinja.pocoo.org/) - templating
* [Requests](http://docs.python-requests.org/) - HTTP requests
* [zplgrf](https://github.com/kylemacfarlane/zplgrf) - PNG to ZPL conversion
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.1
0.1.0
3 changes: 3 additions & 0 deletions overview.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions roulier/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from . import codec
from . import transport
from . import roulier
from . import exception
import logging

__all__ = [roulier]
Expand Down
137 changes: 137 additions & 0 deletions roulier/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
"""API interface."""
from cerberus import Validator


class MyValidator(Validator):
"""Custom validator."""

def _validate_description(self, description, field, value):
"""Allow 'description' in schema.
The rule's arguments are validated against this schema:
{ 'description': 'a string'}
"""
pass


class Api(object):
"""Define expected fields of carriers.
This class should be overriden by each carrier.
"""

def __init__(self):
"""."""
v = MyValidator()
v.allow_unknown = True
v.purge_unknown = True
self._validator = v

def _address(self):
return {
'company': {'type': 'string', 'default': '', 'description': 'Company'},
'name': {'type': 'string', 'default': '', 'required': True, 'empty': False},
'street1': {'type': 'string', 'default': ''},
'street2': {'type': 'string', 'default': ''},
'country': {'type': 'string', 'default': '', 'description': 'ISO 3166-1 alpha-2 '},
'city': {'type': 'string', 'default': ''},
'zip': {'type': 'string', 'default': ''},
'phone': {'type': 'string', 'default': '', 'description': 'Phone'},
'email': {'type': 'string', 'default': ''},
}

def _from_address(self):
address = self._address()
return address

def _to_address(self):
address = self._address()
address['street1'].update({'required': True, 'empty': False})
address['country'].update({'required': True, 'empty': False})
address['city'].update({'required': True, 'empty': False})
address['zip'].update({'required': True, 'empty': False})
return address

def _parcel(self):
return {
"weight": {'type': 'float', 'default': '', 'description': 'Weight in kg', 'required': True, 'empty': False},
}

def _service(self):
return {
"product": {'default': '', 'description': ''},
"agencyId": {'default': '', 'description': ''},
"customerId": {'default': '', 'description': ''},
"shippingId": {'default': ''},
'shippingDate': {'default': '', 'type': 'string', 'required': True, 'empty': False, 'description': 'When the carrier has the package. Format: YYYY/MM/DD'},
'reference1': {'type': 'string', 'default': '', 'description': 'Additionnal info visible by the client. Example : order number'},
'reference2': {'type': 'string', 'default': ''},
'reference3': {'type': 'string', 'default': ''},
"labelFormat": {'description': 'Format of output (usually pdf or zpl)', 'default': ''},
"instructions": {'description': 'Additionnal instructions for delivery', 'default': ''},
}

def _auth(self):
return {
'login': {'type': 'string', 'default': ''},
'password': {'type': 'string', 'default': ''},
}

def _schemas(self):
return {
'service': self._service(),
'auth': self._auth(),
'parcel': self._parcel(),
'from_address': self._from_address(),
'to_address': self._to_address(),
}

def api_schema(self):
"""Return the expected schema of the api.
A validation schema is a mapping, usually a dict.
Schema keys are the keys allowed in the target dictionary.
Schema values express the rules that must be matched
by the corresponding target values.
See http://docs.python-cerberus.org/en/stable/schemas.html
"""
v = MyValidator()
schemas = self._schemas()

return {
s: {
'schema': schemas[s],
'default': v.normalized({}, schemas[s])
}
for s in schemas}

def api_values(self):
"""Return a dict containing expected keys.
It's a normalized version of the schema.
"""
return self.normalize({})

def errors(self, data):
"""Return validation errors."""
self._validator.validate(data, self.api_schema())
return self._validator.errors

def validate(self, data):
"""Ensure the data are valid.
returns: bool
See also errors()
"""
return self._validator.validate(data, self.api_schema())

def normalize(self, data):
"""Retrurn a normalized dict based on input.
See http://docs.python-cerberus.org/en/stable/usage.html
"""
return self._validator.normalized(data, self.api_schema())
4 changes: 3 additions & 1 deletion roulier/carriers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from . import laposte
from . import dummy
from . import geodis
from . import dummy
from . import dpd
6 changes: 6 additions & 0 deletions roulier/carriers/dpd/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from . import dpd
from . import dpd_decoder
from . import dpd_encoder
from . import dpd_transport
from . import dpd_api
Loading

0 comments on commit 11fa49d

Please sign in to comment.