Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.17.0] - 2026-07-08

### Added

- **LoopNet sub-client** (`client.loopnet`) — commercial-real-estate (CoStar) listings, brokers, and reference data across loopnet.com/.ca/.co.uk/.fr/.es (US/CA/UK/FR/ES). Endpoints: `search.search()` (for-lease / for-sale / auctions, all property types, filters, pagination), `listings.get()`, `brokers.get()`, `reference.markets()`, `reference.property_types()`, with fully-typed maximal-coverage Pydantic models (`LoopnetSearchResponse`, `LoopnetListingDetail` incl. offers/facts/brokers/media, `LoopnetBrokerProfile`, `LoopnetListingCard`, etc.). LoopNet is behind Akamai Bot Manager (browser-farm-only) — served via the ScrapeBadger farm. (SCR-102)

## [0.15.7] - 2026-07-07

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export SCRAPEBADGER_API_KEY="sb_live_xxxxxxxxxxxxx"
| **YouTube** | 39 endpoints — search, autocomplete, video detail/related/comments/replies/transcript/captions/streams/live-chat/batch, channel detail + videos/shorts/streams/playlists/community/about/subscriber-count/in-channel-search/resolve, playlists/items/mixes, trending/hashtag/home, shorts, community post/comments, music search, oembed, categories/languages/regions | [YouTube Guide](docs/youtube.md) |
| **TikTok** | 25 endpoints — user profile/videos/followers/following/liked/reposts, video detail/comments/replies/related/transcript/oEmbed, search (general/videos/hashtags/users), music detail/videos, hashtag detail/videos, trending videos/hashtags/songs, ad library, regions | [TikTok Guide](docs/tiktok.md) |
| **Immobiliare** | 8 endpoints across immobiliare.it, indomio.es, indomio.gr, immotop.lu — autocomplete, search, listing detail, agency profile/listings, price stats, markets, reference | [Immobiliare Guide](docs/immobiliare.md) |
| **LoopNet** | 5 endpoints across loopnet.com/.ca/.co.uk/.fr/.es — commercial-real-estate search (for-lease/for-sale/auctions), listing detail, broker profile, markets, property types | [LoopNet Guide](docs/loopnet.md) |

## Error Handling

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "scrapebadger"
version = "0.16.0"
version = "0.17.0"
description = "Official Python SDK for ScrapeBadger - Async web scraping APIs for Twitter and more"
readme = "README.md"
license = { text = "MIT" }
Expand Down
57 changes: 56 additions & 1 deletion src/scrapebadger/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,46 @@ async def main():
from scrapebadger.leboncoin.models import (
StoreRatingReview as LeboncoinStoreRatingReview,
)
from scrapebadger.loopnet.client import LoopNetClient
from scrapebadger.loopnet.models import (
Broker as LoopnetBroker,
)
from scrapebadger.loopnet.models import (
BrokerProfile as LoopnetBrokerProfile,
)
from scrapebadger.loopnet.models import (
BrokerResponse as LoopnetBrokerResponse,
)
from scrapebadger.loopnet.models import (
ListingCard as LoopnetListingCard,
)
from scrapebadger.loopnet.models import (
ListingDetail as LoopnetListingDetail,
)
from scrapebadger.loopnet.models import (
ListingResponse as LoopnetListingResponse,
)
from scrapebadger.loopnet.models import (
MarketInfo as LoopnetMarketInfo,
)
from scrapebadger.loopnet.models import (
MarketsResponse as LoopnetMarketsResponse,
)
from scrapebadger.loopnet.models import (
Pagination as LoopnetPagination,
)
from scrapebadger.loopnet.models import (
PropertyTypeInfo as LoopnetPropertyTypeInfo,
)
from scrapebadger.loopnet.models import (
PropertyTypesResponse as LoopnetPropertyTypesResponse,
)
from scrapebadger.loopnet.models import (
SearchResponse as LoopnetSearchResponse,
)
from scrapebadger.loopnet.models import (
Space as LoopnetSpace,
)
from scrapebadger.realtor.client import RealtorClient
from scrapebadger.realtor.models import (
Address as RealtorAddress,
Expand Down Expand Up @@ -697,7 +737,7 @@ async def main():
ZestimateHistoryPoint as ZillowZestimateHistoryPoint,
)

__version__ = "0.16.0"
__version__ = "0.17.0"

__all__ = [
# TikTok core models
Expand Down Expand Up @@ -803,6 +843,21 @@ async def main():
"LeboncoinSellerResponse",
"LeboncoinSimilarResponse",
"LeboncoinStoreRatingReview",
# LoopNet
"LoopNetClient",
"LoopnetBroker",
"LoopnetBrokerProfile",
"LoopnetBrokerResponse",
"LoopnetListingCard",
"LoopnetListingDetail",
"LoopnetListingResponse",
"LoopnetMarketInfo",
"LoopnetMarketsResponse",
"LoopnetPagination",
"LoopnetPropertyTypeInfo",
"LoopnetPropertyTypesResponse",
"LoopnetSearchResponse",
"LoopnetSpace",
"MarketInfo",
"MarketsResponse",
"NewReleasesResponse",
Expand Down
23 changes: 23 additions & 0 deletions src/scrapebadger/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from scrapebadger.google.client import GoogleClient
from scrapebadger.immobiliare.client import ImmobiliareClient
from scrapebadger.leboncoin.client import LeboncoinClient
from scrapebadger.loopnet.client import LoopNetClient
from scrapebadger.realtor.client import RealtorClient
from scrapebadger.reddit.client import RedditClient
from scrapebadger.tiktok.client import TikTokClient
Expand Down Expand Up @@ -132,6 +133,7 @@ def __init__(
self._zillow: ZillowClient | None = None
self._leboncoin: LeboncoinClient | None = None
self._immobiliare: ImmobiliareClient | None = None
self._loopnet: LoopNetClient | None = None

@property
def config(self) -> ClientConfig:
Expand Down Expand Up @@ -405,6 +407,27 @@ def immobiliare(self) -> ImmobiliareClient:
self._immobiliare = ImmobiliareClient(self._base_client)
return self._immobiliare

@property
def loopnet(self) -> LoopNetClient:
"""Access LoopNet Scraper API operations.

Returns:
LoopNetClient providing access to all LoopNet commercial-real-estate
endpoints (search, listing detail, broker profile, markets, property
types) across loopnet.com/.ca/.co.uk/.fr/.es (US/CA/UK/FR/ES).

Example:
```python
results = await client.loopnet.search.search("Houston, TX")
detail = await client.loopnet.listings.get("12345678")
broker = await client.loopnet.brokers.get("jane-doe", "w7x123")
markets = await client.loopnet.reference.markets()
```
"""
if self._loopnet is None:
self._loopnet = LoopNetClient(self._base_client)
return self._loopnet

@property
def tiktok(self) -> TikTokClient:
"""Access TikTok Scraper API operations.
Expand Down
64 changes: 64 additions & 0 deletions src/scrapebadger/loopnet/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""LoopNet API module for ScrapeBadger SDK.

This module provides a comprehensive async client for scraping LoopNet
commercial-real-estate data through the ScrapeBadger API. All methods are
async and return strongly-typed Pydantic models.

Example:
```python
from scrapebadger import ScrapeBadger

async with ScrapeBadger(api_key="your-key") as client:
# Search listings
results = await client.loopnet.search.search("Houston, TX")
for card in results.results:
print(f"{card.position}. {card.address}")

# Get listing detail
detail = await client.loopnet.listings.get("12345678")
print(detail.listing.price_text)

# Get a broker profile
broker = await client.loopnet.brokers.get("jane-doe", "w7x123")
print(broker.broker.name)
```
"""

from scrapebadger.loopnet.client import LoopNetClient
from scrapebadger.loopnet.models import (
Broker,
BrokerProfile,
BrokerResponse,
ListingCard,
ListingDetail,
ListingResponse,
MarketInfo,
MarketsResponse,
Pagination,
PropertyTypeInfo,
PropertyTypesResponse,
SearchResponse,
Space,
)

__all__ = [
# Brokers
"Broker",
"BrokerProfile",
"BrokerResponse",
# Search
"ListingCard",
# Listing detail
"ListingDetail",
"ListingResponse",
# Client
"LoopNetClient",
# Reference
"MarketInfo",
"MarketsResponse",
"Pagination",
"PropertyTypeInfo",
"PropertyTypesResponse",
"SearchResponse",
"Space",
]
65 changes: 65 additions & 0 deletions src/scrapebadger/loopnet/brokers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""LoopNet Brokers API client.

Provides a broker/professional's profile and their active listings.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from scrapebadger.loopnet.models import BrokerResponse

if TYPE_CHECKING:
from scrapebadger._internal.client import BaseClient


class BrokersClient:
"""Client for the LoopNet broker profile endpoint.

Example:
```python
async with ScrapeBadger(api_key="key") as client:
profile = await client.loopnet.brokers.get("jane-doe", "w7x123")
print(profile.broker.name, profile.broker.company)
```
"""

def __init__(self, client: BaseClient) -> None:
"""Initialize brokers client.

Args:
client: The base HTTP client.
"""
self._client = client

async def get(
self,
slug: str,
broker_id: str,
*,
market: str = "us",
) -> BrokerResponse:
"""Get a LoopNet broker's profile and their listings. Costs 8 credits.

Args:
slug: The broker's URL slug (e.g. "jane-doe").
broker_id: The LoopNet broker id.
market: Coverage market ("us", "ca", "uk", "fr", "es"). Defaults to "us".

Returns:
Broker profile response with the broker and their listings.

Raises:
NotFoundError: If the broker doesn't exist.
AuthenticationError: If the API key is invalid.

Example:
```python
profile = await client.loopnet.brokers.get("jane-doe", "w7x123")
for card in profile.broker.listings:
print(card.address)
```
"""
params: dict[str, Any] = {"market": market}
response = await self._client.get(f"/v1/loopnet/brokers/{slug}/{broker_id}", params=params)
return BrokerResponse.model_validate(response)
Loading
Loading