diff --git a/modules/bienici/__init__.py b/modules/bienici/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2d61180cdff4f797b09c1a66a62b8d45ebc3ca3d --- /dev/null +++ b/modules/bienici/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2018 Antoine BOSSY +# +# This file is part of woob. +# +# woob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# woob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with woob. If not, see . + +from __future__ import unicode_literals + + +from .module import BieniciModule + + +__all__ = ['BieniciModule'] diff --git a/modules/bienici/browser.py b/modules/bienici/browser.py new file mode 100644 index 0000000000000000000000000000000000000000..9f018f774cef37d6f7dbc100d57e11f26aeb3a66 --- /dev/null +++ b/modules/bienici/browser.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2018 Antoine BOSSY +# +# This file is part of woob. +# +# woob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# woob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with woob. If not, see . + +from __future__ import unicode_literals + + +from woob.browser import PagesBrowser, URL +from woob.tools.json import json + +from woob.capabilities.housing import POSTS_TYPES, HOUSE_TYPES +from .pages import Cities, ResultsPage, HousingPage + + +TRANSACTION_TYPE = { + POSTS_TYPES.SALE: 'buy', + POSTS_TYPES.RENT: 'rent', + POSTS_TYPES.SALE: 'buy', + POSTS_TYPES.FURNISHED_RENT: 'rent', + POSTS_TYPES.SHARING: 'rent' +} + + +HOUSE_TYPES = { + HOUSE_TYPES.APART: ['flat'], + HOUSE_TYPES.HOUSE: ['house'], + HOUSE_TYPES.PARKING: ['parking'], + HOUSE_TYPES.LAND: ['terrain'], + HOUSE_TYPES.OTHER: ['others', 'loft', 'shop', 'building', 'castle', 'premises', 'office', 'townhouse'], + HOUSE_TYPES.UNKNOWN: [] +} + + +class BieniciBrowser(PagesBrowser): + BASEURL = 'https://www.bienici.com' + + cities = URL(r'https://res.bienici.com/suggest.json\?q=(?P.+)', Cities) + results = URL(r'/realEstateAds.json\?filters=(?P.+)', ResultsPage) + housing = URL(r'/realEstateAds-one.json\?filters=(?P.*)&onlyRealEstateAd=(?P.*)', HousingPage) + + def get_cities(self, pattern): + return self.cities.go(zipcode=pattern).get_city() + + def search_housing(self, query): + filters = { + 'size': 100, + 'page': 1, + 'resultsPerPage': 24, + 'maxAuthorizedResults': 2400, + 'sortBy': "relevance", + 'sortOrder': "desc", + 'onTheMarket': [True], + 'showAllModels': False, + "zoneIdsByTypes": { + 'zoneIds': [] + }, + 'propertyType': [] + } + + dict_query = query.to_dict() + if dict_query['area_min']: + filters['minArea'] = dict_query['area_min'] + + if dict_query['area_max']: + filters['maxArea'] = dict_query['area_max'] + + if dict_query['cost_min']: + filters['minPrice'] = dict_query['cost_min'] + + if dict_query['cost_max']: + filters['maxPrice'] = dict_query['cost_max'] + + filters['filterType'] = TRANSACTION_TYPE[dict_query['type']] + + for housing_type in dict_query['house_types']: + filters['propertyType'] += HOUSE_TYPES[housing_type] + + for city in dict_query['cities']: + filters['zoneIdsByTypes']['zoneIds'].append(city.id) + + return self.results.go(filters=json.dumps(filters)).get_housings() + + def get_housing(self, housing_id): + # This is to serialize correctly the JSON, and match the URL easier. + filters = { + 'onTheMarket': [True] + } + return self.housing.go(housing_id=housing_id, filters=json.dumps(filters)).get_housing() diff --git a/modules/bienici/module.py b/modules/bienici/module.py new file mode 100644 index 0000000000000000000000000000000000000000..0501a472e31791ad2c090fe27a384086190a9488 --- /dev/null +++ b/modules/bienici/module.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2018 Antoine BOSSY +# +# This file is part of woob. +# +# woob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# woob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with woob. If not, see . + +from __future__ import unicode_literals + + +from woob.tools.backend import Module +from woob.capabilities.housing import CapHousing, Housing, HousingPhoto + +from .browser import BieniciBrowser + + +__all__ = ['BieniciModule'] + + +class BieniciModule(Module, CapHousing): + NAME = 'bienici' + DESCRIPTION = 'bienici website' + MAINTAINER = 'Antoine BOSSY' + EMAIL = 'mail+github@abossy.fr' + LICENSE = 'AGPLv3+' + VERSION = '3.1' + + BROWSER = BieniciBrowser + + def get_housing(self, id): + """ + Get an housing from an ID. + + :param housing: ID of the housing + :type housing: str + :rtype: :class:`Housing` or None if not found. + """ + return self.browser.get_housing(id) + + def search_city(self, pattern): + """ + Search a city from a pattern. + + :param pattern: pattern to search + :type pattern: str + :rtype: iter[:class:`City`] + """ + return self.browser.get_cities(pattern) + + def search_housings(self, query): + """ + Search housings. + + :param query: search query + :type query: :class:`Query` + :rtype: iter[:class:`Housing`] + """ + return self.browser.search_housing(query) + + def fill_photo(self, photo, fields): + """ + Fills the photo. + """ + if 'data' in fields and photo.url and not photo.data: + photo.data = self.browser.open(photo.url).content + return photo + + def fill_housing(self, housing, fields): + """ + Fills the housing. + """ + return self.get_housing(housing.id) + + OBJECTS = {HousingPhoto: fill_photo, Housing: fill_housing} diff --git a/modules/bienici/pages.py b/modules/bienici/pages.py new file mode 100644 index 0000000000000000000000000000000000000000..9ca2e0ba34977b242146768f143d8860d17643ef --- /dev/null +++ b/modules/bienici/pages.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2018 Antoine BOSSY +# +# This file is part of woob. +# +# woob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# woob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with woob. If not, see . + +from __future__ import unicode_literals + + +from woob.browser.pages import JsonPage +from woob.browser.elements import ItemElement, DictElement, method +from woob.browser.filters.json import Dict, ItemNotFound +from woob.capabilities.base import NotAvailable +from woob.browser.filters.standard import CleanText, Date, CleanDecimal +from woob.capabilities.housing import City, Housing, HousingPhoto, ENERGY_CLASS + + +class Cities(JsonPage): + @method + class get_city(DictElement): + + class item(ItemElement): + klass = City + + obj_id = Dict('zoneIds/0') + obj_name = CleanText(Dict('name')) + + +class MyItemElement(ItemElement): + klass = Housing + + def condition(self): + return not Dict('userRelativeData/isAdModifier')(self) + + obj_id = Dict('id') + obj_title = Dict('title') + obj_area = Dict('surfaceArea') + obj_cost = Dict('price') + + def obj_price_per_meter(self): + try: + return Dict('pricePerSquareMeter')(self) + except ItemNotFound: + return NotAvailable + + obj_currency = 'EUR' + obj_date = Date(Dict('publicationDate')) + obj_location = CleanDecimal(Dict('postalCode')) + obj_text = Dict('description', '') + + def obj_photos(self): + return [HousingPhoto(photo['url']) for photo in Dict('photos')(self)] + + obj_rooms = Dict('roomsQuantity', 0) + obj_bedrooms = Dict('bedroomsQuantity', 0) + + def obj_DPE(self): + try: + return ENERGY_CLASS[Dict('energyClassification')(self)] + except (KeyError, ItemNotFound): + return NotAvailable + + def obj_GES(self): + try: + return ENERGY_CLASS[Dict('greenhouseGazClassification')(self)] + except (KeyError, ItemNotFound): + return NotAvailable + + +class ResultsPage(JsonPage): + @method + class get_housings(DictElement): + item_xpath = 'realEstateAds' + + class item(MyItemElement): + pass + + +class HousingPage(JsonPage): + @method + class get_housing(MyItemElement): + pass