Skip to content

Commit 736f3ed

Browse files
authored
Merge pull request #5 from GregEremeev/v_1
Version 1.0.0
2 parents 29c090d + 84fbeea commit 736f3ed

File tree

12 files changed

+5105
-144
lines changed

12 files changed

+5105
-144
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
include README.md
22
include LICENSE.txt
33
include requirements.txt
4+
include rosreestr_api/cacert.pem

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pip install rosreestr-api
1111

1212
2 Different ways how to get basic info about realty objects:
1313
```python
14-
from rosreestr_api.clients import RosreestrAPIClient, AddressWrapper
14+
from rosreestr_api.clients.rosreestr import RosreestrAPIClient, AddressWrapper
1515

1616
api_client = RosreestrAPIClient()
1717

@@ -45,7 +45,7 @@ api_client.get_region_types(region_id=region_id)
4545

4646
3 Different ways how to get geo info about realty objects:
4747
```python
48-
from rosreestr_api.clients import PKKRosreestrAPIClient
48+
from rosreestr_api.clients.rosreestr import PKKRosreestrAPIClient
4949

5050
api_client = PKKRosreestrAPIClient()
5151

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
requests==2.22.0
1+
requests==2.31.0
2+
fake-useragent==1.5.0

rosreestr_api/cacert.pem

Lines changed: 4894 additions & 0 deletions
Large diffs are not rendered by default.
File renamed without changes.

rosreestr_api/clients/http.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import logging
2+
import os.path
3+
import ssl
4+
import time
5+
from typing import Union
6+
from urllib.parse import urlencode
7+
from importlib.util import find_spec
8+
9+
import requests
10+
from requests import Session
11+
from requests.adapters import HTTPAdapter
12+
13+
import rosreestr_api
14+
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
class BaseHTTPClient:
20+
SESSION_CLS = Session
21+
22+
GET_HTTP_METHOD = 'GET'
23+
POST_HTTP_METHOD = 'POST'
24+
PATCH_HTTP_METHOD = 'PATCH'
25+
PUT_HTTP_METHOD = 'PUT'
26+
27+
BODY_LESS_METHODS = [GET_HTTP_METHOD]
28+
LOG_REQUEST_TEMPLATE = '%(method)s %(url)s%(request_body)s%(duration)s'
29+
LOG_RESPONSE_TEMPLATE = (LOG_REQUEST_TEMPLATE +
30+
' - HTTP %(status_code)s%(response_body)s%(duration)s')
31+
32+
def __init__(self, timeout=3, keep_alive=False, default_headers=None):
33+
self.timeout = timeout
34+
self.keep_alive = keep_alive
35+
self.default_headers = default_headers or {}
36+
self._session = None
37+
38+
@property
39+
def session(self) -> requests.Session:
40+
if self.keep_alive:
41+
if not self._session:
42+
self._session = self.SESSION_CLS()
43+
return self._session
44+
else:
45+
return self.SESSION_CLS()
46+
47+
def get(self, url, params=None, **kwargs) -> requests.Response:
48+
if params:
49+
url_with_query_params = url + '?' + urlencode(params)
50+
else:
51+
url_with_query_params = url
52+
53+
return self._make_request(self.GET_HTTP_METHOD, url_with_query_params, **kwargs)
54+
55+
def post(self, url, **kwargs) -> requests.Response:
56+
return self._make_request(self.POST_HTTP_METHOD, url, **kwargs)
57+
58+
def patch(self, url, **kwargs) -> requests.Response:
59+
return self._make_request(self.PATCH_HTTP_METHOD, url, **kwargs)
60+
61+
def put(self, url, **kwargs) -> requests.Response:
62+
return self._make_request(self.PUT_HTTP_METHOD, url, **kwargs)
63+
64+
def _log_request(self, method, url, body, duration=None, log_method=logger.info):
65+
message_params = {
66+
'method': method, 'url': url, 'request_body': _get_body_for_logging(body),
67+
'duration': _get_duration_for_logging(duration)}
68+
log_method(self.LOG_REQUEST_TEMPLATE, message_params)
69+
70+
def _log_response(self, response, duration, log_method=logger.info):
71+
message_params = {
72+
'method': response.request.method,
73+
'url': response.request.url,
74+
'request_body': _get_body_for_logging(response.request.body),
75+
'status_code': response.status_code,
76+
'response_body': _get_body_for_logging(response.content),
77+
'duration': _get_duration_for_logging(duration)}
78+
log_method(self.LOG_RESPONSE_TEMPLATE, message_params)
79+
80+
def _make_request(self, method, url, **kwargs) -> requests.Response:
81+
kwargs.setdefault('timeout', self.timeout)
82+
session = self.session
83+
timeout = kwargs.pop('timeout', self.timeout)
84+
85+
headers = self.default_headers.copy()
86+
headers.update(kwargs.pop('headers', {}))
87+
88+
request = requests.Request(method, url, headers=headers, **kwargs)
89+
prepared_request = request.prepare()
90+
self._log_request(method, url, prepared_request.body)
91+
start_time = time.time()
92+
try:
93+
response = session.send(prepared_request, timeout=timeout)
94+
duration = time.time() - start_time
95+
if response.status_code >= 400:
96+
log_method = logging.error
97+
else:
98+
log_method = logging.debug
99+
100+
self._log_response(response, duration=duration, log_method=log_method)
101+
return response
102+
except requests.exceptions.RequestException as e:
103+
duration = time.time() - start_time
104+
if e.response:
105+
self._log_response(e.response, duration=duration, log_method=logging.error)
106+
else:
107+
self._log_request(method, url, prepared_request.body, log_method=logging.exception)
108+
raise
109+
finally:
110+
if not self.keep_alive:
111+
session.close()
112+
113+
114+
class HTTPSAdapter(HTTPAdapter):
115+
def init_poolmanager(self, *args, **kwargs):
116+
ssl_context = ssl.create_default_context()
117+
# https://www.openssl.org/docs/man3.0/man3/SSL_CTX_set_security_level.html
118+
# rosreestr supports only SECLEVEL 1
119+
ssl_context.set_ciphers('DEFAULT@SECLEVEL=1')
120+
# We want to use the most secured protocol from security level 1
121+
ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
122+
kwargs['ssl_context'] = ssl_context
123+
return super().init_poolmanager(*args, **kwargs)
124+
125+
126+
class CustomSession(Session):
127+
CACERT_PATH = os.path.join(
128+
os.path.dirname(find_spec(rosreestr_api.__name__).origin),
129+
'cacert.pem'
130+
)
131+
def __init__(self):
132+
super().__init__()
133+
self.verify = self.CACERT_PATH
134+
self.mount(prefix='https://', adapter=HTTPSAdapter())
135+
136+
137+
class RosreestrHTTPClient(BaseHTTPClient):
138+
SESSION_CLS = CustomSession
139+
140+
141+
def _get_body_for_logging(body: Union[bytes, str]) -> str:
142+
try:
143+
if isinstance(body, bytes):
144+
return (b' BODY: ' + body).decode('utf-8')
145+
elif isinstance(body, str):
146+
return ' BODY: ' + body
147+
else:
148+
return ''
149+
except UnicodeDecodeError:
150+
return ''
151+
152+
153+
def _get_duration_for_logging(duration: str) -> str:
154+
if duration is not None:
155+
return ' {0:.6f}s'.format(duration)
156+
else:
157+
return ''

rosreestr_api/clients.py renamed to rosreestr_api/clients/rosreestr.py

Lines changed: 23 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,137 +1,15 @@
1-
import time
21
import logging
3-
from typing import Union
42
from dataclasses import dataclass
5-
from urllib.parse import urlencode, quote_plus
3+
from urllib.parse import quote_plus
64

75
import requests
6+
from fake_useragent import UserAgent
87

8+
from rosreestr_api.clients.http import RosreestrHTTPClient
99

1010
logger = logging.getLogger(__name__)
1111

1212

13-
def _strip_cadastral_id(cadastral_id):
14-
stripped_cadastral_id = []
15-
cadastral_id = cadastral_id.split(':')
16-
for part in cadastral_id:
17-
if part:
18-
stripped_cadastral_id.append(part[:-1].lstrip('0') + part[-1])
19-
return ':'.join(stripped_cadastral_id)
20-
21-
22-
def _get_body_for_logging(body: Union[bytes, str]) -> str:
23-
try:
24-
if isinstance(body, bytes):
25-
return (b' BODY: ' + body).decode('utf-8')
26-
elif isinstance(body, str):
27-
return ' BODY: ' + body
28-
else:
29-
return ''
30-
except UnicodeDecodeError:
31-
return ''
32-
33-
34-
def _get_duration_for_logging(duration: str) -> str:
35-
if duration is not None:
36-
return ' {0:.6f}s'.format(duration)
37-
else:
38-
return ''
39-
40-
41-
class HTTPClient:
42-
43-
GET_HTTP_METHOD = 'GET'
44-
POST_HTTP_METHOD = 'POST'
45-
PATCH_HTTP_METHOD = 'PATCH'
46-
PUT_HTTP_METHOD = 'PUT'
47-
48-
BODY_LESS_METHODS = [GET_HTTP_METHOD]
49-
LOG_REQUEST_TEMPLATE = '%(method)s %(url)s%(request_body)s%(duration)s'
50-
LOG_RESPONSE_TEMPLATE = (LOG_REQUEST_TEMPLATE +
51-
' - HTTP %(status_code)s%(response_body)s%(duration)s')
52-
53-
def __init__(self, timeout=3, keep_alive=False, default_headers=None):
54-
self.timeout = timeout
55-
self.keep_alive = keep_alive
56-
self.default_headers = default_headers or {}
57-
self._session = None
58-
59-
def _log_request(self, method, url, body, duration=None, log_method=logger.info):
60-
message_params = {
61-
'method': method, 'url': url, 'request_body': _get_body_for_logging(body),
62-
'duration': _get_duration_for_logging(duration)}
63-
log_method(self.LOG_REQUEST_TEMPLATE, message_params)
64-
65-
def _log_response(self, response, duration, log_method=logger.info):
66-
message_params = {
67-
'method': response.request.method,
68-
'url': response.request.url,
69-
'request_body': _get_body_for_logging(response.request.body),
70-
'status_code': response.status_code,
71-
'response_body': _get_body_for_logging(response.content),
72-
'duration': _get_duration_for_logging(duration)}
73-
log_method(self.LOG_RESPONSE_TEMPLATE, message_params)
74-
75-
def _make_request(self, method, url, **kwargs) -> requests.Response:
76-
kwargs.setdefault('timeout', self.timeout)
77-
session = self.session
78-
timeout = kwargs.pop('timeout', self.timeout)
79-
80-
headers = self.default_headers.copy()
81-
headers.update(kwargs.pop('headers', {}))
82-
83-
request = requests.Request(method, url, headers=headers, **kwargs)
84-
prepared_request = request.prepare()
85-
self._log_request(method, url, prepared_request.body)
86-
start_time = time.time()
87-
try:
88-
response = session.send(prepared_request, timeout=timeout)
89-
duration = time.time() - start_time
90-
if response.status_code >= 400:
91-
log_method = logging.error
92-
else:
93-
log_method = logging.debug
94-
95-
self._log_response(response, duration=duration, log_method=log_method)
96-
return response
97-
except requests.exceptions.RequestException as e:
98-
duration = time.time() - start_time
99-
if e.response:
100-
self._log_response(e.response, duration=duration, log_method=logging.error)
101-
else:
102-
self._log_request(method, url, prepared_request.body, log_method=logging.exception)
103-
raise
104-
finally:
105-
if not self.keep_alive:
106-
session.close()
107-
108-
@property
109-
def session(self) -> requests.Session:
110-
if self.keep_alive:
111-
if not self._session:
112-
self._session = requests.Session()
113-
return self._session
114-
else:
115-
return requests.Session()
116-
117-
def get(self, url, params=None, **kwargs) -> requests.Response:
118-
if params:
119-
url_with_query_params = url + '?' + urlencode(params)
120-
else:
121-
url_with_query_params = url
122-
123-
return self._make_request(self.GET_HTTP_METHOD, url_with_query_params, **kwargs)
124-
125-
def post(self, url, **kwargs) -> requests.Response:
126-
return self._make_request(self.POST_HTTP_METHOD, url, **kwargs)
127-
128-
def patch(self, url, **kwargs) -> requests.Response:
129-
return self._make_request(self.PATCH_HTTP_METHOD, url, **kwargs)
130-
131-
def put(self, url, **kwargs) -> requests.Response:
132-
return self._make_request(self.PUT_HTTP_METHOD, url, **kwargs)
133-
134-
13513
@dataclass
13614
class AddressWrapper:
13715

@@ -156,7 +34,7 @@ def __post_init__(self):
15634

15735
class RosreestrAPIClient:
15836

159-
BASE_URL = 'http://rosreestr.ru/api/online'
37+
BASE_URL = 'https://rosreestr.gov.ru/api/online'
16038
MACRO_REGIONS_URL = f'{BASE_URL}/macro_regions/'
16139
REGIONS_URL = f'{BASE_URL}/regions/' + '{}/'
16240
REGION_TYPES_URL = f'{BASE_URL}/region_types/' + '{}/'
@@ -171,7 +49,11 @@ class RosreestrAPIClient:
17149
REPUBLIC = 'республика'
17250

17351
def __init__(self, timeout=5, keep_alive=False):
174-
self._http_client = HTTPClient(timeout=timeout, keep_alive=keep_alive)
52+
self._http_client = RosreestrHTTPClient(
53+
timeout=timeout,
54+
keep_alive=keep_alive,
55+
default_headers={'User-Agent': UserAgent().random}
56+
)
17557
self._macro_regions = None
17658
self._macro_regions_to_regions = None
17759

@@ -296,7 +178,11 @@ class PKKRosreestrAPIClient:
296178
SEARCH_PARCEL_BY_CADASTRAL_ID_URL = SEARCH_OBJECT_BY_CADASTRAL_ID.format(object_type=1)
297179

298180
def __init__(self, timeout=5, keep_alive=False):
299-
self._http_client = HTTPClient(timeout=timeout, keep_alive=keep_alive)
181+
self._http_client = RosreestrHTTPClient(
182+
timeout=timeout,
183+
keep_alive=keep_alive,
184+
default_headers={'User-Agent': UserAgent().random}
185+
)
300186

301187
def get_parcel_by_coordinates(self, *, lat, long, limit=11, tolerance=2) -> dict:
302188
url = self.SEARCH_PARCEL_BY_COORDINATES_URL.format(
@@ -317,3 +203,12 @@ def get_building_by_coordinates(self, *, lat, long, limit=11, tolerance=2) -> di
317203
url = self.SEARCH_BUILDING_BY_COORDINATES_URL.format(
318204
lat=lat, long=long, limit=limit, tolerance=tolerance)
319205
return self._http_client.get(url).json()
206+
207+
208+
def _strip_cadastral_id(cadastral_id):
209+
stripped_cadastral_id = []
210+
cadastral_id = cadastral_id.split(':')
211+
for part in cadastral_id:
212+
if part:
213+
stripped_cadastral_id.append(part[:-1].lstrip('0') + part[-1])
214+
return ':'.join(stripped_cadastral_id)

setup.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,22 @@
88
name='rosreestr-api',
99
author='Greg Eremeev',
1010
author_email='gregory.eremeev@gmail.com',
11-
version='0.3.4',
11+
version='1.0.0',
1212
license='BSD-3-Clause',
1313
url='https://github.com/GregEremeev/rosreestr-api',
1414
install_requires=requirements,
15-
description='Toolset to work with rosreestr.ru/api',
15+
description='Toolset to work with rosreestr.gov.ru/api and pkk.rosreestr.ru/api',
1616
packages=find_packages(),
1717
extras_require={'dev': ['ipdb>=0.13.2', 'pytest>=5.4.1', 'httpretty>=1.0.2']},
1818
classifiers=[
1919
'Intended Audience :: Developers',
2020
'License :: OSI Approved :: BSD License',
2121
'Programming Language :: Python :: 3.7',
22+
'Programming Language :: Python :: 3.8',
23+
'Programming Language :: Python :: 3.9',
24+
'Programming Language :: Python :: 3.10',
25+
'Programming Language :: Python :: 3.11',
26+
'Programming Language :: Python :: 3.12',
2227
'Programming Language :: Python :: Implementation :: CPython'
2328
],
2429
zip_safe=False,

tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)