pax_global_header 0000666 0000000 0000000 00000000064 13435444765 0014531 g ustar 00root root 0000000 0000000 52 comment=139760e841ad4875b59fe11e052af1bb56dc06ea
woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-seloger/ 0000775 0000000 0000000 00000000000 13435444765 0022600 5 ustar 00root root 0000000 0000000 woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-seloger/modules/ 0000775 0000000 0000000 00000000000 13435444765 0024250 5 ustar 00root root 0000000 0000000 woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-seloger/modules/seloger/ 0000775 0000000 0000000 00000000000 13435444765 0025710 5 ustar 00root root 0000000 0000000 woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-seloger/modules/seloger/__init__.py 0000664 0000000 0000000 00000000077 13435444765 0030025 0 ustar 00root root 0000000 0000000 from .module import SeLogerModule
__all__ = ['SeLogerModule']
woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-seloger/modules/seloger/browser.py 0000664 0000000 0000000 00000005606 13435444765 0027754 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Romain Bignon
#
# This file is part of a weboob module.
#
# This weboob module 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.
#
# This weboob module 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 this weboob module. If not, see .
from weboob.capabilities.housing import TypeNotSupported, POSTS_TYPES
from weboob.tools.compat import urlencode
from weboob.browser import PagesBrowser, URL
from .pages import SearchResultsPage, HousingPage, CitiesPage
from weboob.browser.profiles import Android
from .constants import TYPES, RET
__all__ = ['SeLogerBrowser']
class SeLogerBrowser(PagesBrowser):
BASEURL = 'http://www.seloger.com'
PROFILE = Android()
cities = URL('https://autocomplete.svc.groupe-seloger.com/auto/complete/0/Ville/6\?text=(?P.*)', CitiesPage)
search = URL('http://ws.seloger.com/search.xml\?(?P.*)', SearchResultsPage)
housing = URL('http://ws.seloger.com/annonceDetail.xml\?idAnnonce=(?P<_id>\d+)&noAudiotel=(?P\d)',
HousingPage)
def search_geo(self, pattern):
return self.cities.open(pattern=pattern).iter_cities()
def search_housings(self, type, cities, nb_rooms, area_min, area_max,
cost_min, cost_max, house_types, advert_types):
if type not in TYPES:
raise TypeNotSupported()
data = {'ci': ','.join(cities),
'idtt': TYPES.get(type, 1),
'org': 'advanced_search',
'surfacemax': area_max or '',
'surfacemin': area_min or '',
'tri': 'd_dt_crea',
}
if type == POSTS_TYPES.SALE:
data['pxmax'] = cost_max or ''
data['pxmin'] = cost_min or ''
else:
data['px_loyermax'] = cost_max or ''
data['px_loyermin'] = cost_min or ''
if nb_rooms:
data['nb_pieces'] = nb_rooms
ret = []
for house_type in house_types:
if house_type in RET:
ret.append(RET.get(house_type))
if ret:
data['idtypebien'] = ','.join(ret)
return self.search.go(request=urlencode(data)).iter_housings(
query_type=type, advert_types=advert_types
)
def get_housing(self, _id, obj=None):
return self.housing.go(_id=_id, noAudiotel=1).get_housing(obj=obj)
woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-seloger/modules/seloger/constants.py 0000664 0000000 0000000 00000000544 13435444765 0030301 0 ustar 00root root 0000000 0000000 from weboob.capabilities.housing import POSTS_TYPES, HOUSE_TYPES
TYPES = {POSTS_TYPES.RENT: 1,
POSTS_TYPES.SALE: 2,
POSTS_TYPES.FURNISHED_RENT: 1,
POSTS_TYPES.VIAGER: 5}
RET = {HOUSE_TYPES.HOUSE: '2',
HOUSE_TYPES.APART: '1',
HOUSE_TYPES.LAND: '4',
HOUSE_TYPES.PARKING: '3',
HOUSE_TYPES.OTHER: '10'}
woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-seloger/modules/seloger/favicon.png 0000664 0000000 0000000 00000003337 13435444765 0030051 0 ustar 00root root 0000000 0000000 PNG
IHDR @ @ iq bKGD 5gDW pHYs tIME Ƚ
iTXtComment Created with GIMPd.e CIDATxyPWǿ;#9RDBGKD/ԪcjVxN;N[;j;:Gvъ-@%N!Ad#.{nv}oCտb x! pE!qgfC2aq1Z`p>x`=09u:T[˦8yt5N>Qg eV&L
J I͝`>|T/ve& >). R"A+"[* ?`)e9_A 0F_aA.g>)ј-vjix8j*#!TIZ "wD1ta}pܸ
l4zXOE50G$qnnm}X͞9o߶.}c7{AWո
x
n J<8Gzڂ%9^f :,nzfDF
<+A䌩f>u: jXD_7,8N H3!T+Ղr2- Gpr6B>QԩqYN/ gm<rsX2ᅣX3h+}APw\*NA߸0/]Yd!d!EPl|l W}j0=ͿHN8Zw>m N4nǧ@48e9WA|/PɃ|\P>
p%5Z x=n
G}8$sړP/Hg7x^UVϫA,Ʉ0,\㷇AW߁ِHkS{">[7tSPϤCE$W QUGU
L]
٤H4Y%"_Bvqd@U"SɦMXQ(ݰJӻ `4YTx.Wjz)q71~!z0_gkk!Ct }"t :-괠j{mvz%FhYթu^y0[BYHռ˫"5S_9 ^5y7(ZOz@:%Sx:
j3 m5vR ;7Ot`ju`/M{sU@XzőD\˴aԧ!
{GH@ۖ|01y_"6x(P*B!ap^#PnkIy}Yyx?Ecs`7Q'P^՛~mj bUHc4gr8y+ਬaJX/rOf;yN
M-p=6jN>RإoD`H2@4< X(I3`-Rlj+)&C]`q ` ?88P IENDB` woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-seloger/modules/seloger/module.py 0000664 0000000 0000000 00000004662 13435444765 0027557 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Romain Bignon
#
# This file is part of a weboob module.
#
# This weboob module 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.
#
# This weboob module 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 this weboob module. If not, see .
from weboob.capabilities.housing import CapHousing, Housing, HousingPhoto
from weboob.tools.backend import Module
from .browser import SeLogerBrowser
__all__ = ['SeLogerModule']
class SeLogerModule(Module, CapHousing):
NAME = 'seloger'
MAINTAINER = u'Romain Bignon'
EMAIL = 'romain@weboob.org'
VERSION = '1.5'
DESCRIPTION = 'French housing website'
LICENSE = 'AGPLv3+'
ICON = 'http://static.poliris.com/z/portail/svx/portals/sv6_gen/favicon.png'
BROWSER = SeLogerBrowser
def search_housings(self, query):
cities = [c.id for c in query.cities if c.backend == self.name]
if len(cities) == 0:
return list([])
return self.browser.search_housings(query.type, cities, query.nb_rooms,
query.area_min, query.area_max,
query.cost_min, query.cost_max,
query.house_types,
query.advert_types)
def get_housing(self, housing):
if isinstance(housing, Housing):
id = housing.id
else:
id = housing
housing = None
return self.browser.get_housing(id, housing)
def search_city(self, pattern):
return self.browser.search_geo(pattern)
def fill_photo(self, photo, fields):
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):
return self.browser.get_housing(housing.id, housing)
OBJECTS = {HousingPhoto: fill_photo, Housing: fill_housing}
woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-seloger/modules/seloger/pages.py 0000664 0000000 0000000 00000015557 13435444765 0027376 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Romain Bignon
#
# This file is part of a weboob module.
#
# This weboob module 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.
#
# This weboob module 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 this weboob module. If not, see .
from weboob.browser.pages import XMLPage, JsonPage, pagination
from weboob.browser.elements import ItemElement, ListElement, DictElement, method
from weboob.browser.filters.json import Dict
from weboob.browser.filters.html import XPath
from weboob.browser.filters.standard import (CleanText, CleanDecimal, Currency,
DateTime, Env, Format, Regexp)
from weboob.capabilities.base import NotAvailable, NotLoaded
from weboob.capabilities.housing import (Housing, HousingPhoto, City,
UTILITIES, ENERGY_CLASS, POSTS_TYPES,
ADVERT_TYPES)
from weboob.tools.capabilities.housing.housing import PricePerMeterFilter
from .constants import TYPES, RET
class CitiesPage(JsonPage):
@method
class iter_cities(DictElement):
ignore_duplicate = True
class item(ItemElement):
klass = City
obj_id = Dict('Params/ci')
obj_name = Dict('Display')
class SeLogerItem(ItemElement):
klass = Housing
obj_id = CleanText('idAnnonce')
def obj_type(self):
idType = int(CleanText('idTypeTransaction')(self))
type = next(k for k, v in TYPES.items() if v == idType)
if type == POSTS_TYPES.FURNISHED_RENT:
# SeLoger does not let us discriminate between furnished and not
# furnished.
return POSTS_TYPES.RENT
return type
def obj_house_type(self):
idType = CleanText('idTypeBien')(self)
try:
return next(k for k, v in RET.items() if v == idType)
except StopIteration:
return NotLoaded
obj_title = Format(
"%s %s%s - %s",
CleanText('titre'),
CleanText('surface'),
CleanText('surfaceUnite'),
CleanText('ville'),
)
obj_date = DateTime(CleanText('dtFraicheur'))
obj_cost = CleanDecimal('prix', default=NotLoaded)
obj_currency = Currency(Regexp(CleanText('prixUnite'), r'(\W).*', r'\1'))
obj_area = CleanDecimal('surface', default=NotLoaded)
obj_price_per_meter = PricePerMeterFilter()
obj_text = CleanText('descriptif')
obj_rooms = CleanDecimal('nbPiece|nbPieces', default=NotLoaded)
obj_bedrooms = CleanDecimal('nbChambre|nbChambres', default=NotLoaded)
def obj_location(self):
location = CleanText('adresse', default="")(self)
quartier = CleanText('quartier', default=None)(self)
if not location and quartier is not None:
location = quartier
ville = CleanText('ville')(self)
cp = CleanText('cp')(self)
return u'%s %s (%s)' % (location, ville, cp)
obj_station = CleanText('proximite', default=NotLoaded)
obj_url = CleanText('permaLien')
class SearchResultsPage(XMLPage):
@pagination
@method
class iter_housings(ListElement):
item_xpath = "//annonce"
def next_page(self):
page = CleanText('//pageSuivante', default=None, replace=[('http://ws.seloger.com/', '')])(self)
if page:
return page
class item(SeLogerItem):
def condition(self):
if self.env['query_type'] == POSTS_TYPES.SALE:
# Ignore VIAGER
return CleanText('idTypeTransaction')(self) == '2'
return True
def validate(self, obj):
return (len(self.env['advert_types']) == 1 and
self.env['advert_types'][0] == obj.advert_type) or \
self.env['advert_types'] > 1
obj_type = Env('query_type')
def obj_advert_type(self):
is_agency = (
';' not in CleanText('contact/nom')(self)
)
if is_agency:
return ADVERT_TYPES.PROFESSIONAL
else:
return ADVERT_TYPES.PERSONAL
def obj_photos(self):
photos = []
for photo in XPath('./photos/photo/stdUrl')(self):
photos.append(HousingPhoto(CleanText('.')(photo)))
return photos
def obj_utilities(self):
currency = CleanText('prixUnite')(self)
if "+ch" in currency:
return UTILITIES.EXCLUDED
elif "cc*" in currency:
return UTILITIES.INCLUDED
else:
return UTILITIES.UNKNOWN
class HousingPage(XMLPage):
@method
class get_housing(SeLogerItem):
def obj_photos(self):
photos = []
for photo in XPath('./photos/photo')(self):
url = CleanText('bigUrl', default=None)(photo)
if not url:
url = CleanText('stdUrl', default=None)(photo)
photos.append(HousingPhoto(url))
return photos
def obj_advert_type(self):
is_agency = (
CleanText('contact/rcsSiren')(self) or
CleanText('contact/rcsNic')(self) or
CleanText('contact/idAnnuaire')(self)
)
if is_agency:
return ADVERT_TYPES.PROFESSIONAL
else:
return ADVERT_TYPES.PERSONAL
def obj_DPE(self):
DPE = CleanText('//bilanConsoEnergie', default="")(self)
return getattr(ENERGY_CLASS, DPE, NotAvailable)
def obj_GES(self):
GES = CleanText('//bilanEmissionGES', default="")(self)
return getattr(ENERGY_CLASS, GES, NotAvailable)
def obj_details(self):
details = {}
for detail in XPath('//detailAnnonce/details/detail')(self):
details[CleanText('libelle')(detail)] = CleanText('valeur', default='N/A')(detail)
details['Reference'] = CleanText('//detailAnnonce/reference')(self)
return details
obj_phone = CleanText('//contact/telephone')
def obj_utilities(self):
mention = CleanText('prixMention')(self)
if "charges comprises" in mention:
return UTILITIES.INCLUDED
else:
return UTILITIES.UNKNOWN
woob-139760e841ad4875b59fe11e052af1bb56dc06ea-modules-seloger/modules/seloger/test.py 0000664 0000000 0000000 00000007065 13435444765 0027251 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Romain Bignon
#
# This file is part of a weboob module.
#
# This weboob module 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.
#
# This weboob module 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 this weboob module. If not, see .
from weboob.capabilities.housing import Query, POSTS_TYPES, ADVERT_TYPES
from weboob.tools.test import BackendTest
from weboob.tools.capabilities.housing.housing_test import HousingTest
class SeLogerTest(BackendTest, HousingTest):
MODULE = 'seloger'
FIELDS_ALL_HOUSINGS_LIST = [
"id", "type", "advert_type", "house_type", "url", "title", "area",
"utilities", "date", "location", "text"
]
FIELDS_ANY_HOUSINGS_LIST = [
"cost", # Some posts don't have cost in seloger
"currency", # Same
"photos",
"station",
"rooms",
"bedrooms"
]
FIELDS_ALL_SINGLE_HOUSING = [
"id", "url", "type", "advert_type", "house_type", "title", "area",
"utilities", "date", "location", "text", "phone", "details"
]
FIELDS_ANY_SINGLE_HOUSING = [
"cost", # Some posts don't have cost in seloger
"currency", # Same
"photos",
"rooms",
"bedrooms",
"station",
"DPE",
"GES"
]
DO_NOT_DISTINGUISH_FURNISHED_RENT = True
def test_seloger_rent(self):
query = Query()
query.area_min = 20
query.cost_max = 1500
query.type = POSTS_TYPES.RENT
query.cities = []
for city in self.backend.search_city('paris'):
city.backend = self.backend.name
query.cities.append(city)
self.check_against_query(query)
def test_seloger_sale(self):
query = Query()
query.area_min = 20
query.type = POSTS_TYPES.SALE
query.cities = []
for city in self.backend.search_city('paris'):
city.backend = self.backend.name
query.cities.append(city)
self.check_against_query(query)
def test_seloger_furnished_rent(self):
query = Query()
query.area_min = 20
query.cost_max = 1500
query.type = POSTS_TYPES.FURNISHED_RENT
query.cities = []
for city in self.backend.search_city('paris'):
city.backend = self.backend.name
query.cities.append(city)
self.check_against_query(query)
def test_seloger_viager(self):
query = Query()
query.type = POSTS_TYPES.VIAGER
query.cities = []
for city in self.backend.search_city('85'):
city.backend = self.backend.name
query.cities.append(city)
self.check_against_query(query)
def test_seloger_rent_personal(self):
query = Query()
query.area_min = 20
query.cost_max = 1500
query.type = POSTS_TYPES.RENT
query.advert_types = [ADVERT_TYPES.PROFESSIONAL]
query.cities = []
for city in self.backend.search_city('paris'):
city.backend = self.backend.name
query.cities.append(city)
self.check_against_query(query)