Projet

Général

Profil

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

Benjamin Dauvergne, 20 février 2018 00:20

Télécharger (21 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          | 193 +++++++++++++
 passerelle/settings.py                             |   1 +
 tests/test_api_particulier.py                      | 312 +++++++++++++++++++++
 tests/utils.py                                     |  15 +
 7 files changed, 552 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 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
import requests
24

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

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

  
32

  
33
class APIParticulier(BaseResource):
34
    PLATFORMS = [
35
        {
36
            'name': 'prod',
37
            'label': _('Production'),
38
            'url': 'https://particulier.api.gouv.fr/api/'
39
        },
40
        {
41
            'name': 'test',
42
            'label': _('Test'),
43
            'url': 'https://particulier-sandbox.api.gouv.fr/api/',
44
            'api_key': 'test-token',
45
        },
46
    ]
47
    PLATFORMS = OrderedDict([(platform['name'], platform) for platform in PLATFORMS])
48

  
49
    _platform = models.CharField(
50
        verbose_name=_('Platform'),
51
        max_length=8,
52
        choices=[(key, platform['label']) for key, platform in PLATFORMS.iteritems()])
53

  
54
    _api_key = models.CharField(
55
        max_length=64,
56
        default='',
57
        blank=True,
58
        verbose_name=_('API key'))
59

  
60
    @property
61
    def platform(self):
62
        return self.PLATFORMS[self._platform]
63

  
64
    @property
65
    def url(self):
66
        return self.platform['url']
67

  
68
    @property
69
    def api_key(self):
70
        return self.platform.get('api_key', self._api_key)
71

  
72
    def get(self, path, **kwargs):
73
        user = kwargs.pop('user', None)
74
        url = urljoin(self.url, path)
75
        headers = {'X-API-KEY': self.api_key}
76
        if user:
77
            headers['X-User'] = user
78
        response = None
79
        try:
80
            response = self.requests.get(
81
                url,
82
                headers=headers,
83
                **kwargs)
84
        except requests.RequestException as e:
85
            raise APIError(
86
                u'API-particulier platform "%s" connection error: %s' %
87
                (self._platform, response.status_code),
88
                data={
89
                    'platform': self._platform,
90
                    'error': unicode(e),
91
                })
92
        try:
93
            data = response.json()
94
        except ValueError as e:
95
            content = repr(response.content[:1000])
96
            raise APIError(
97
                u'API-particulier platform "%s" returned non-JSON content with status %s: %s' %
98
                (self._platform, response.status_code, content),
99
                data={
100
                    'status_code': response.status_code,
101
                    'exception': unicode(e),
102
                    'platform': self._platform,
103
                    'content': content,
104
                })
105
        if response.status_code != 200:
106
            if data.get('error') == 'not_found':
107
                return {
108
                    'err': 1,
109
                    'err_desc': data.get('message', 'not-found'),
110
                }
111
            raise APIError(
112
                u'API-particulier platform "%s" returned a non 200 status %s: %s' %
113
                (self._platform, response.status_code, data),
114
                data={
115
                    'status_code': response.status_code,
116
                    'platform': self._platform,
117
                    'content': data,
118
                })
119
        return {
120
            'err': 0,
121
            'data': data,
122
        }
123

  
124
    @endpoint(perm='can_access',
125
              description=_('Get citizen\'s fiscal informations'),
126
              parameters={
127
                  'numero_fiscal': {
128
                      'description': _('fiscal identifier'),
129
                      'example_value': '12',
130
                  },
131
                  'reference_avis': {
132
                      'description': _('tax notice number'),
133
                      'example_value': '15',
134
                  },
135
                  'user': {
136
                      'description': _('requesting user'),
137
                      'example_value': 'John Doe (agent)',
138
                  },
139
              })
140
    def impots_svair(self, request, numero_fiscal, reference_avis, user=None):
141
        return self.get('impots/svair', params={
142
            'numeroFiscal': numero_fiscal,
143
            'referenceAvis': reference_avis,
144
        }, user=user)
145

  
146
    @endpoint(perm='can_access',
147
              description=_('Get citizen\'s fiscal address'),
148
              parameters={
149
                  'numero_fiscal': {
150
                      'description': _('fiscal identifier'),
151
                      'example_value': '12',
152
                  },
153
                  'reference_avis': {
154
                      'description': _('tax notice number'),
155
                      'example_value': '15',
156
                  },
157
                  'user': {
158
                      'description': _('requesting user'),
159
                      'example_value': 'John Doe (agent)',
160
                  },
161
              })
162
    def impots_adresse(self, request, numero_fiscal, reference_avis, user=None):
163
        return self.get('impots/adress', params={
164
            'numeroFiscal': numero_fiscal,
165
            'referenceAvis': reference_avis,
166
        }, user=user)
167

  
168
    @endpoint(perm='can_access',
169
              description=_('Get family allowances recipient informations'),
170
              parameters={
171
                  'code_postal': {
172
                      'description': _('postal code'),
173
                      'example_value': '99148',
174
                  },
175
                  'numero_allocataire': {
176
                      'description': _('recipient identifier'),
177
                      'example_value': '0000354',
178
                  },
179
                  'user': {
180
                      'description': _('requesting user'),
181
                      'example_value': 'John Doe (agent)',
182
                  },
183
              })
184
    def caf_famille(self, request, code_postal, numero_allocataire, user=None):
185
        return self.get('caf/famille', params={
186
            'codePostal': code_postal,
187
            'numeroAllocataire': numero_allocataire,
188
        }, user=user)
189

  
190
    category = _('Business Process Connectors')
191

  
192
    class Meta:
193
        verbose_name = _('API Particulier')
passerelle/settings.py
116 116
    'passerelle.datasources',
117 117
    # connectors
118 118
    'passerelle.apps.airquality',
119
    'passerelle.apps.api_particulier',
119 120
    'passerelle.apps.base_adresse',
120 121
    'passerelle.apps.bdp',
121 122
    'passerelle.apps.choosit',
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 django.core.urlresolvers import reverse
23

  
24
from passerelle.apps.api_particulier.models import APIParticulier
25

  
26
from utils import make_resource, endpoint_get
27

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

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

  
101
CAF_FAMILLE = {
102
    "adresse": {
103
        "codePostalVille": "12345 CONDAT",
104
        "complementIdentiteGeo": "ESCALIER B",
105
        "identite": "Madame MARIE DUPONT",
106
        "numeroRue": "123 RUE BIDON",
107
        "pays": "FRANCE"
108
    },
109
    "allocataires": [
110
        {
111
            "dateDeNaissance": "12111971",
112
            "nomPrenom": "MARIE DUPONT",
113
            "sexe": "F"
114
        },
115
        {
116
            "dateDeNaissance": "18101969",
117
            "nomPrenom": "JEAN DUPONT",
118
            "sexe": "M"
119
        }
120
    ],
121
    "annee": 2017,
122
    "enfants": [
123
        {
124
            "dateDeNaissance": "11122016",
125
            "nomPrenom": "LUCIE DUPONT",
126
            "sexe": "F"
127
        }
128
    ],
129
    "mois": 4,
130
    "quotientFamilial": 1754
131
}
132

  
133

  
134
@urlmatch(netloc='^particulier.*\.api\.gouv\.fr$',
135
          path='^/api/impots/svair$')
136
def api_particulier_impots_svair(url, request):
137
    return response(200, SVAIR_RESPONSE, request=request)
138

  
139

  
140
@urlmatch(netloc='^particulier.*\.api\.gouv\.fr$',
141
          path='^/api/impots/adresse$')
142
def api_particulier_impots_adresse(url, request):
143
    return response(200, IMPOTS_ADRESSE, request=request)
144

  
145

  
146
@urlmatch(netloc='^particulier.*\.api\.gouv\.fr$',
147
          path='^/api/caf/famille$')
148
def api_particulier_caf_famille(url, request):
149
    return response(200, CAF_FAMILLE, request=request)
150

  
151

  
152
@urlmatch(netloc='^particulier.*\.api\.gouv\.fr$')
153
def api_particulier_error_500(url, request):
154
    return response(500, 'something bad happened', request=request)
155

  
156

  
157
@urlmatch(netloc='^particulier.*\.api\.gouv\.fr$')
158
def api_particulier_error_not_json(url, request):
159
    return response(200, 'something bad happened', request=request)
160

  
161

  
162
@urlmatch(netloc='^particulier.*\.api\.gouv\.fr$')
163
def api_particulier_error_not_found(url, request):
164
    return response(404, {
165
        'error': 'not_found',
166
        'message': u'Les paramètres fournis sont incorrects ou ne correspondent pas à un avis'
167
    }, request=request)
168

  
169

  
170
@pytest.yield_fixture
171
def mock_api_particulier():
172
    with HTTMock(api_particulier_impots_svair, api_particulier_impots_adresse, api_particulier_caf_famille):
173
        yield None
174

  
175

  
176
@pytest.fixture
177
def resource(db):
178
    return make_resource(
179
        APIParticulier,
180
        slug='test',
181
        title='API Particulier Prod',
182
        description='API Particulier Prod',
183
        _platform='test')
184

  
185

  
186
def test_error(app, resource, mock_api_particulier):
187
    with HTTMock(api_particulier_error_500):
188
        def do(endpoint, params):
189
            resp = endpoint_get(
190
                '/api-particulier/test/%s' % endpoint,
191
                app,
192
                resource,
193
                endpoint,
194
                params=params)
195
            assert resp.status_code == 200
196
            assert resp.json['err'] == 1
197
            assert resp.json['data']['status_code'] == 500
198
        vector = [
199
            (['impots_svair', 'impots_adresse'], {
200
                'numero_fiscal': 12,
201
                'reference_avis': 15,
202
            }),
203
            (['caf_famille'], {
204
                'code_postal': 12,
205
                'numero_allocataire': 15
206
            }),
207
        ]
208
        for endpoints, params in vector:
209
            for endpoint in endpoints:
210
                do(endpoint, params)
211
    with HTTMock(api_particulier_error_not_json):
212
        def do(endpoint, params):
213
            resp = endpoint_get(
214
                '/api-particulier/test/%s' % endpoint,
215
                app,
216
                resource,
217
                endpoint,
218
                params=params)
219
            assert resp.status_code == 200
220
            assert resp.json['err'] == 1
221
            assert resp.json['data']['exception'] == 'No JSON object could be decoded'
222
        vector = [
223
            (['impots_svair', 'impots_adresse'], {
224
                'numero_fiscal': 12,
225
                'reference_avis': 15,
226
            }),
227
            (['caf_famille'], {
228
                'code_postal': 12,
229
                'numero_allocataire': 15
230
            }),
231
        ]
232
        for endpoints, params in vector:
233
            for endpoint in endpoints:
234
                do(endpoint, params)
235
    with HTTMock(api_particulier_error_not_found):
236
        def do(endpoint, params):
237
            resp = endpoint_get(
238
                '/api-particulier/test/%s' % endpoint,
239
                app,
240
                resource,
241
                endpoint,
242
                params=params)
243
            assert resp.status_code == 200
244
            assert resp.json['err'] == 1
245
            assert resp.json['err_desc'].endswith(u'à un avis')
246
        vector = [
247
            (['impots_svair', 'impots_adresse'], {
248
                'numero_fiscal': 12,
249
                'reference_avis': 15,
250
            }),
251
            (['caf_famille'], {
252
                'code_postal': 12,
253
                'numero_allocataire': 15
254
            }),
255
        ]
256
        for endpoints, params in vector:
257
            for endpoint in endpoints:
258
                do(endpoint, params)
259

  
260

  
261
def test_impots_svair(app, resource, mock_api_particulier):
262
    resp = endpoint_get(
263
        '/api-particulier/test/impots_svair',
264
        app,
265
        resource,
266
        'impots_svair',
267
        params={
268
            'numero_fiscal': 12,
269
            'reference_avis': 15,
270
            'user': 'John Doe',
271
        })
272
    assert resp.json['data']['montantImpot'] == 2165
273

  
274

  
275
def test_impots_adresse(app, resource, mock_api_particulier):
276
    resp = endpoint_get(
277
        '/api-particulier/test/impots_adresse',
278
        app,
279
        resource,
280
        'impots_adresse',
281
        params={
282
            'numero_fiscal': 12,
283
            'reference_avis': 15,
284
            'user': 'John Doe',
285
        })
286
    assert resp.json['data']['adresses'][0]['adresse']['citycode'] == '75108'
287

  
288

  
289
def test_caf_famille(app, resource, mock_api_particulier):
290
    resp = endpoint_get(
291
        '/api-particulier/test/caf_famille',
292
        app,
293
        resource,
294
        'caf_famille',
295
        params={
296
            'code_postal': '99148',
297
            'numero_allocataire': '000354',
298
            'user': 'John Doe',
299
        })
300
    assert resp.json['data']['adresse']['codePostalVille'] == '12345 CONDAT'
301

  
302

  
303
def test_detail_page(app, resource):
304
    response = app.get(reverse('view-connector', kwargs={
305
        'connector': 'api-particulier',
306
        'slug': 'test',
307
    }))
308
    assert 'API Particulier Prod' in response.content
309
    assert 'family allowance' in response.content
310
    assert 'fiscal information' in response.content
311
    assert 'fiscal address' in response.content
312

  
tests/utils.py
39 39
    def mocked(url, request):
40 40
        return response
41 41
    return httmock.HTTMock(mocked)
42

  
43

  
44
def make_resource(model_class, **kwargs):
45
    resource = model_class.objects.create(**kwargs)
46
    setup_access_rights(resource)
47
    return resource
48

  
49

  
50
def endpoint_get(expected_url, app, ressource, endpoint, **kwargs):
51
    url = generic_endpoint_url(
52
        connector=ressource.__class__.get_connector_slug(),
53
        endpoint=endpoint,
54
        slug=ressource.slug)
55
    assert url == expected_url, 'endpoint URL has changed'
56
    return app.get(url, **kwargs)
42
-