Projet

Général

Profil

0001-add-connector-for-API-Particulier-14838.patch

Benjamin Dauvergne, 24 mars 2017 16:30

Télécharger (19,3 ko)

Voir les différences:

Subject: [PATCH] add connector for API-Particulier (#14838)

API Particulier is an API published by the french government for accessing
fiscal and social informations about citizens. It can be used to improve
efficiency of procedures in local administrations.
 passerelle/apps/api_particulier/__init__.py        |   0
 .../api_particulier/migrations/0001_initial.py     |  31 +++
 .../apps/api_particulier/migrations/__init__.py    |   0
 passerelle/apps/api_particulier/models.py          | 156 +++++++++++++++
 .../api_particulier/api_particulier_detail.html    |  44 +++++
 passerelle/settings.py                             |   1 +
 tests/test_api_particulier.py                      | 214 +++++++++++++++++++++
 tests/utils.py                                     |  24 +++
 8 files changed, 470 insertions(+)
 create mode 100644 passerelle/apps/api_particulier/__init__.py
 create mode 100644 passerelle/apps/api_particulier/migrations/0001_initial.py
 create mode 100644 passerelle/apps/api_particulier/migrations/__init__.py
 create mode 100644 passerelle/apps/api_particulier/models.py
 create mode 100644 passerelle/apps/api_particulier/templates/api_particulier/api_particulier_detail.html
 create mode 100644 tests/test_api_particulier.py
passerelle/apps/api_particulier/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import models, migrations
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    dependencies = [
10
        ('base', '0002_auto_20151009_0326'),
11
    ]
12

  
13
    operations = [
14
        migrations.CreateModel(
15
            name='APIParticulier',
16
            fields=[
17
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
18
                ('title', models.CharField(max_length=50)),
19
                ('slug', models.SlugField()),
20
                ('description', models.TextField()),
21
                ('log_level', models.CharField(default=b'NOTSET', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL'), (b'FATAL', b'FATAL')])),
22
                ('_platform', models.CharField(max_length=8, verbose_name='platform', choices=[(b'test', 'Test'), (b'prod', 'Production'), (b'dev', 'Development'), (b'mock', 'Mock')])),
23
                ('_api_key', models.CharField(default=b'', max_length=64, verbose_name='API key', blank=True)),
24
                ('users', models.ManyToManyField(to='base.ApiUser', blank=True)),
25
            ],
26
            options={
27
                'abstract': False,
28
            },
29
            bases=(models.Model,),
30
        ),
31
    ]
passerelle/apps/api_particulier/models.py
1
# passerelle.apps.api_particulier
2
# Copyright (C) 2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
'''Gateway to API-Particulier web-service from SGMAP:
18
   https://particulier.api.gouv.fr/
19
'''
20

  
21
from urlparse import urljoin
22
from collections import OrderedDict
23

  
24
from django.db import models
25
from django.utils.translation import ugettext_lazy as _
26

  
27
from passerelle.base.models import BaseResource
28
from passerelle.utils.api import endpoint
29
from passerelle.utils.jsonresponse import APIError
30

  
31

  
32
class APIParticulier(BaseResource):
33
    PLATFORMS = [
34
        {
35
            'name': 'test',
36
            'label': _('Test'),
37
            'url': 'https://particulier-test.api.gouv.fr/api/',
38
            'api_key': 'test-token',
39
        },
40
        {
41
            'name': 'prod',
42
            'label': _('Production'),
43
            'url': 'https://particulier.api.gouv.fr/api/'
44
        },
45
        {
46
            'name': 'dev',
47
            'label': _('Development'),
48
            'url': 'https://particulier-dev.api.gouv.fr/api/'
49
        },
50
        {
51
            'name': 'mock',
52
            'label': _('Mock'),
53
            'url': 'https://particulier-mock.api.gouv.fr/api/'
54
        },
55
    ]
56
    PLATFORMS = OrderedDict([(platform['name'], platform) for platform in PLATFORMS])
57

  
58
    _platform = models.CharField(
59
        verbose_name=_('platform'),
60
        max_length=8,
61
        choices=[(key, platform['label']) for key, platform in PLATFORMS.iteritems()])
62

  
63
    _api_key = models.CharField(
64
        max_length=64,
65
        default='',
66
        blank=True,
67
        verbose_name=_('API key'))
68

  
69
    @property
70
    def platform(self):
71
        return self.PLATFORMS[self._platform]
72

  
73
    @property
74
    def url(self):
75
        return self.platform['url']
76

  
77
    @property
78
    def api_key(self):
79
        return self.platform.get('api_key', self._api_key)
80

  
81
    def get(self, path, **kwargs):
82
        user = kwargs.pop('user', None)
83
        url = urljoin(self.url, path)
84
        headers = {'X-API-KEY': self.api_key}
85
        if user:
86
            headers['X-User'] = user
87
        response = self.requests.get(
88
            url,
89
            headers=headers,
90
            **kwargs)
91

  
92
        if response.status_code != 200:
93
            raise APIError(
94
                u'API-particulier platform "%s" returned non-200 code: %s' %
95
                (self._platform, response.status_code),
96
                data={
97
                    'platform': self._platform,
98
                    'code': response.status_code,
99
                    'content': repr(response.content[:1000]),
100
                })
101
        try:
102
            return response.json()
103
        except ValueError as e:
104
            content = repr(response.content[:1000])
105
            raise APIError(
106
                u'API-particulier platform "%s" returned non-JSON content: %s' %
107
                (self._platform, content),
108
                data={
109
                    'exception': unicode(e),
110
                    'platform': self._platform,
111
                    'content': content,
112
                })
113

  
114
    @endpoint(serializer_type='json-api', perm='can_access')
115
    def impots_svair(self, request, numero_fiscal, reference_avis, user=None):
116
        return self.get('impots/svair', params={
117
            'numeroFiscal': numero_fiscal,
118
            'referenceAvis': reference_avis,
119
        }, user=user)
120

  
121
    @endpoint(serializer_type='json-api', perm='can_access')
122
    def impots_adresse(self, request, numero_fiscal, reference_avis, user=None):
123
        return self.get('impots/adresse', params={
124
            'numeroFiscal': numero_fiscal,
125
            'referenceAvis': reference_avis,
126
        }, user=user)
127

  
128
    @endpoint(serializer_type='json-api', perm='can_access')
129
    def caf_qf(self, request, code_postal, numero_allocataire, user=None):
130
        return self.get('caf/qf', params={
131
            'codePostal': code_postal,
132
            'numeroAllocataire': numero_allocataire,
133
        }, user=user)
134

  
135
    @endpoint(serializer_type='json-api', perm='can_access')
136
    def caf_adresse(self, request, code_postal, numero_allocataire, user=None):
137
        return self.get('caf/adresse', params={
138
            'codePostal': code_postal,
139
            'numeroAllocataire': numero_allocataire,
140
        }, user=user)
141

  
142
    @endpoint(serializer_type='json-api', perm='can_access')
143
    def caf_famille(self, request, code_postal, numero_allocataire, user=None):
144
        return self.get('caf/famille', params={
145
            'codePostal': code_postal,
146
            'numeroAllocataire': numero_allocataire,
147
        }, user=user)
148

  
149
    @classmethod
150
    def get_icon_class(cls):
151
        return 'ressources'
152

  
153
    category = _('Business Process Connectors')
154

  
155
    class Meta:
156
        verbose_name = _('API Particulier')
passerelle/apps/api_particulier/templates/api_particulier/api_particulier_detail.html
1
{% extends "passerelle/manage/service_view.html" %}
2
{% load i18n passerelle %}
3

  
4
{% block endpoints %}
5
<ul>
6
  <li>
7
    {% trans "Get fiscal informations:" %}
8
    {% url "generic-endpoint" connector="api_particulier" slug=object.slug endpoint="impots_svair" as endpoint_path %}
9
    <a href="{{endpoint_path}}?numero_fiscal=12&reference_avis=15"
10
           >{{endpoint_path}}?numero_fiscal=12&reference_avis=15</a>
11
  </li>
12
  <li>
13
    {% trans "Get fiscal address:" %}
14
    {% url "generic-endpoint" connector="api_particulier" slug=object.slug endpoint="impots_adresse" as endpoint_path %}
15
    <a href="{{endpoint_path}}?numero_fiscal=12&reference_avis=15"
16
           >{{endpoint_path}}?numero_fiscal=12&reference_avis=15</a>
17
  </li>
18
  <li>
19
    {% trans "Get CAF family quotient:" %}
20
    {% url "generic-endpoint" connector="api_particulier" slug=object.slug endpoint="caf_qf" as endpoint_path %}
21
    <a href="{{endpoint_path}}?code_postal=13260&numero_allocataire=15"
22
           >{{endpoint_path}}?code_postal=13260&numero_allocataire=15</a>
23
  </li>
24
  <li>
25
    {% trans "Get CAF address:" %}
26
    {% url "generic-endpoint" connector="api_particulier" slug=object.slug endpoint="caf_adresse" as endpoint_path %}
27
    <a href="{{endpoint_path}}?code_postal=13260&numero_allocataire=15"
28
           >{{endpoint_path}}?code_postal=13260&numero_allocataire=15</a>
29
  </li>
30
  <li>
31
    {% trans "Get CAF family informations:" %}
32
    {% url "generic-endpoint" connector="api_particulier" slug=object.slug endpoint="caf_famille" as endpoint_path %}
33
    <a href="{{endpoint_path}}?code_postal=13260&numero_allocataire=15"
34
           >{{endpoint_path}}?code_postal=13260&numero_allocataire=15</a>
35
  </li>
36
</ul>
37
{% endblock %}
38

  
39
{% block security %}
40
<p>
41
  {% trans 'Access is limited to the following API users:' %}
42
</p>
43
{% access_rights_table resource=object permission='can_access' %}
44
{% endblock %}
passerelle/settings.py
111 111
    'orange',
112 112
    'family',
113 113
    'passerelle.apps.opengis',
114
    'api_particulier',
114 115
    # backoffice templates and static
115 116
    'gadjo',
116 117
)
tests/test_api_particulier.py
1
# -*- coding: utf-8 -*-
2

  
3
# tests/test_api_particulier.py
4
# Copyright (C) 2017  Entr'ouvert
5
#
6
# This program is free software: you can redistribute it and/or modify it
7
# under the terms of the GNU Affero General Public License as published
8
# by the Free Software Foundation, either version 3 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU Affero General Public License for more details.
15
#
16
# You should have received a copy of the GNU Affero General Public License
17
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18

  
19
import pytest
20
from httmock import urlmatch, HTTMock, response
21

  
22
from api_particulier.models import APIParticulier
23

  
24
from utils import make_ressource, endpoint_get
25

  
26
SVAIR_RESPONSE = {
27
    "declarant1": {
28
        "nom": "Martin",
29
        "nomNaissance": "Martin",
30
        "prenoms": "Pierre",
31
        "dateNaissance": "22/03/1985"
32
    },
33
    "declarant2": {
34
        "nom": "Martin",
35
        "nomNaissance": "Honore",
36
        "prenoms": "Marie",
37
        "dateNaissance": "03/04/1986"
38
    },
39
    "foyerFiscal": {
40
        "annee": 2015,
41
        "adresse": "12 rue Balzac 75008 Paris"
42
    },
43
    "dateRecouvrement": "10/10/2015",
44
    "dateEtablissement": "08/07/2015",
45
    "nombreParts": 2,
46
    "situationFamille": "Marié(e)s",
47
    "nombrePersonnesCharge": 2,
48
    "revenuBrutGlobal": 29880,
49
    "revenuImposable": 29880,
50
    "impotRevenuNetAvantCorrections": 2165,
51
    "montantImpot": 2165,
52
    "revenuFiscalReference": 29880,
53
    "anneeImpots": "2015",
54
    "anneeRevenus": "2014"
55
}
56

  
57
IMPOTS_ADRESSE = {
58
    "adresses": [
59
        {
60
            "adresse": {
61
                "citycode": "75108",
62
                "street": "Rue Balzac",
63
                "name": "12 Rue Balzac",
64
                "housenumber": "12",
65
                "city": "Paris",
66
                "type": "housenumber",
67
                "context": "75, Île-de-France",
68
                "score": 0.9401454545454544,
69
                "label": "12 Rue Balzac 75008 Paris",
70
                "postcode": "75008"
71
            },
72
            "geometry": {
73
                "type": "Point",
74
                "coordinates": [
75
                    2.300816,
76
                    48.873951
77
                ]
78
            }
79
        }
80
    ],
81
    "declarant1": {
82
        "nom": "Martin",
83
        "nomNaissance": "Martin",
84
        "prenoms": "Pierre",
85
        "dateNaissance": "22/03/1985"
86
    },
87
    "declarant2": {
88
        "nom": "Martin",
89
        "nomNaissance": "Honore",
90
        "prenoms": "Marie",
91
        "dateNaissance": "03/04/1986"
92
    },
93
    "foyerFiscal": {
94
        "annee": 2015,
95
        "adresse": "12 rue Balzac 75008 Paris"
96
    }
97
}
98

  
99

  
100
@urlmatch(netloc='^particulier.*\.api\.gouv\.fr$',
101
          path='^/api/impots/svair$')
102
def api_particulier_impots_svair(url, request):
103
    return response(200, SVAIR_RESPONSE, request=request)
104

  
105

  
106
@urlmatch(netloc='^particulier.*\.api\.gouv\.fr$',
107
          path='^/api/impots/adresse$')
108
def api_particulier_impots_adresse(url, request):
109
    return response(200, IMPOTS_ADRESSE, request=request)
110

  
111

  
112
@urlmatch(netloc='^particulier.*\.api\.gouv\.fr$')
113
def api_particulier_error_500(url, request):
114
    return response(500, 'something bad happened', request=request)
115

  
116
@urlmatch(netloc='^particulier.*\.api\.gouv\.fr$')
117
def api_particulier_error_not_json(url, request):
118
    return response(200, 'something bad happened', request=request)
119

  
120
@pytest.yield_fixture
121
def mock_api_particulier():
122
    with HTTMock(api_particulier_impots_svair, api_particulier_impots_adresse):
123
        yield None
124

  
125

  
126
@pytest.fixture
127
def ressource(db):
128
    return make_ressource(
129
        APIParticulier,
130
        slug='test',
131
        title='API Particulier Prod',
132
        description='API Particulier Prod',
133
        _platform='test')
134

  
135

  
136
def test_error(app, ressource, mock_api_particulier):
137
    with HTTMock(api_particulier_error_500):
138
        def do(endpoint, params):
139
            resp = endpoint_get(
140
                '/api-particulier/test/%s' % endpoint,
141
                app,
142
                ressource,
143
                endpoint,
144
                params=params)
145
            assert resp.status_code == 200
146
            assert resp.json['err'] == 1
147
            assert resp.json['data']['code'] == 500
148
        vector = [
149
            (['impots_svair', 'impots_adresse'], {
150
                'numero_fiscal': 12,
151
                'reference_avis': 15,
152
                'user': 'john.doe',
153
            }),
154
            (['caf_qf', 'caf_adresse', 'caf_famille'], {
155
                'code_postal': 12,
156
                'numero_allocataire': 15
157
            }),
158
        ]
159
        for endpoints, params in vector:
160
            for endpoint in endpoints:
161
                do(endpoint, params)
162
    with HTTMock(api_particulier_error_not_json):
163
        def do(endpoint, params):
164
            resp = endpoint_get(
165
                '/api-particulier/test/%s' % endpoint,
166
                app,
167
                ressource,
168
                endpoint,
169
                params=params)
170
            assert resp.status_code == 200
171
            assert resp.json['err'] == 1
172
            assert resp.json['data']['exception'] == 'No JSON object could be decoded'
173
        vector = [
174
            (['impots_svair', 'impots_adresse'], {
175
                'numero_fiscal': 12,
176
                'reference_avis': 15,
177
                'user': 'john.doe',
178
            }),
179
            (['caf_qf', 'caf_adresse', 'caf_famille'], {
180
                'code_postal': 12,
181
                'numero_allocataire': 15
182
            }),
183
        ]
184
        for endpoints, params in vector:
185
            for endpoint in endpoints:
186
                do(endpoint, params)
187

  
188

  
189
def test_impots_svair(app, ressource, mock_api_particulier):
190
    resp = endpoint_get(
191
        '/api-particulier/test/impots_svair',
192
        app,
193
        ressource,
194
        'impots_svair',
195
        params={
196
            'numero_fiscal': 12,
197
            'reference_avis': 15
198
        })
199
    assert resp.json['data']['montantImpot'] == 2165
200

  
201

  
202
def test_impots_adresse(app, ressource, mock_api_particulier):
203
    resp = endpoint_get(
204
        '/api-particulier/test/impots_adresse',
205
        app,
206
        ressource,
207
        'impots_adresse',
208
        params={
209
            'numero_fiscal': 12,
210
            'reference_avis': 15
211
        })
212
    assert resp.json['data']['adresses'][0]['adresse']['citycode'] == '75108'
213

  
214
# FIXME: CAF web services are currently broken, add test eventually when we can test them
tests/utils.py
39 39
    def mocked(url, request):
40 40
        return response
41 41
    return httmock.HTTMock(mocked)
42

  
43

  
44
def make_ressource(model_class, **kwargs):
45
    api, created = ApiUser.objects.get_or_create(
46
        username='all',
47
        keytype='',
48
        key='')
49
    ressource = model_class.objects.create(**kwargs)
50
    obj_type = ContentType.objects.get_for_model(model_class)
51
    AccessRight.objects.get_or_create(
52
        codename='can_access',
53
        apiuser=api,
54
        resource_type=obj_type,
55
        resource_pk=ressource.pk)
56
    return ressource
57

  
58

  
59
def endpoint_get(expected_url, app, ressource, endpoint, **kwargs):
60
    url = generic_endpoint_url(
61
        connector=ressource.__class__.get_connector_slug(),
62
        endpoint=endpoint,
63
        slug=ressource.slug)
64
    assert url == expected_url, 'endpoint URL has changed'
65
    return app.get(url, **kwargs)
42
-