avatarHubert Dudek

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

5367

Abstract

s-keyword">return</span> <span class="hljs-string">"userId int, id int, title string, body string"</span>

<span class="hljs-keyword">def</span> <span class="hljs-title function_">reader</span>(<span class="hljs-params">self, schema: StructType</span>):
    <span class="hljs-string">"""
    Create and return a DataSourceReader for batch reads.
    """</span>
    <span class="hljs-keyword">return</span> MyRestDataSourceReader(schema, self.options)

<span class="hljs-keyword">def</span> <span class="hljs-title function_">writer</span>(<span class="hljs-params">self, schema: StructType, overwrite: <span class="hljs-built_in">bool</span></span>):
    <span class="hljs-string">"""
    Create and return a DataSourceWriter for batch writes (if needed).
    """</span>
    <span class="hljs-keyword">return</span> MyRestDataSourceWriter(self.options, overwrite)

<span class="hljs-comment"># -----------------------------------------------------------------------------</span> <span class="hljs-comment"># 2) Define a DataSourceReader to handle reads</span> <span class="hljs-comment"># -----------------------------------------------------------------------------</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">MyRestDataSourceReader</span>(<span class="hljs-title class_ inherited__">DataSourceReader</span>): <span class="hljs-keyword">def</span> <span class="hljs-title function_">init</span>(<span class="hljs-params">self, schema: StructType, options: <span class="hljs-built_in">dict</span></span>): self.schema = schema <span class="hljs-comment"># options is a dictionary of strings</span> self.options = options

<span class="hljs-keyword">def</span> <span class="hljs-title function_">read</span>(<span class="hljs-params">self, partition</span>):
    <span class="hljs-string">"""
    Called on each partition to return an iterator of rows.
    For simplicity, this example does NOT implement multiple partitions.
    """</span>
    base_url = <span class="hljs-string">"https://jsonplaceholder.typicode.com"</span>
    endpoint = self.options.get(<span class="hljs-string">"endpoint"</span>, <span class="hljs-string">"posts"</span>)  <span class="hljs-comment"># default to 'posts'</span>
    url = <span class="hljs-string">f"<span class="hljs-subst">{base_url}</span>/<span class="hljs-subst">{endpoint}</span>"</span>

    <span class="hljs-comment"># Make a GET request</span>
    resp = requests.get(url)
    resp.raise_for_status()
    data = resp.json()

    <span class="hljs-comment"># data is a list of dicts (JSONPlaceholder format).</span>
    <span class="hljs-comment"># We yield tuples matching the schema [userId, id, title, body].</span>
    <span class="hljs-keyword">for</span> item <span class="hljs-keyword">in</span> data:
        <span class="hljs-keyword">yield</span> (
            item.get(<span class="hljs-string">"userId"</span>),
            item.get(<span class="hljs-string">"id"</span>),
            item.get(<span class="hljs-string">"title"</span>),
            item.get(<span class="hljs-string">"body"</span>),
        )

<span class="hljs-keyword">def</span> <span class="hljs-title function_">partitions</span>(<span class="hljs-params">self</span>):
    <span class="hljs-string">"""
    If you want multiple partitions, you would define them here.
    For now, we'll return a single partition.
    """</span>
    <span class="hljs-keyword">from</span> pyspark.sql.datasource <span class="hljs-keyword">import</span> InputPartition
    <span class="hljs-keyword">return</span> [InputPartition(<span class="hljs-number">0</span>)]

<span class="hljs-comment"># -----------------------------------------------------------------------------</span> <span class="hljs-comment"># 3) (Optional) Define a DataSourceWriter to handle writes</span> <span class="hljs-comment"># -----------------------------------------------------------------------------</span> <span class="hljs-meta">@dataclass</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">SimpleCommitMessage</span>(<span class="hljs-title class_ inherited__">WriterCommitMessage</span>): partition_id: <span class="hljs-built_in">int</span> count: <span class="hljs-built_in">int</span>

<span class="hljs-keyword">class</span> <span class="hljs-title class_">MyRestDataSourceWriter</span>(<span class="hljs-title class_ inherited__">DataSourceWriter</span>): <span class="hljs-string">""" This is a minimal example of writing to a REST API. JSONPlaceholder won't actually persist the data, but let's illustrate. """</span>

<span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">self, options: <span class="hljs-built_in">dict</span>, overwrite: <span class="hljs-built_in">bool</span></span>):
    self.options = options
    self.overwrite = overwrite

<span class="hljs-keyword">def</span> <span class="hljs-title function_">write</span>(<span class="hljs-params">self, rows: Iterator[Row]</span>) -&gt; WriterCommitMessage:
    <span class="hljs-string">"""
    Called on each partition to write data. 

Options

    Return a commit message for the partition.
    """</span>
    <span class="hljs-keyword">from</span> pyspark <span class="hljs-keyword">import</span> TaskContext
    context = TaskContext.get()
    partition_id = context.partitionId()

    base_url = <span class="hljs-string">"https://jsonplaceholder.typicode.com"</span>
    endpoint = self.options.get(<span class="hljs-string">"endpoint"</span>, <span class="hljs-string">"posts"</span>)
    url = <span class="hljs-string">f"<span class="hljs-subst">{base_url}</span>/<span class="hljs-subst">{endpoint}</span>"</span>
    method = self.options.get(<span class="hljs-string">"method"</span>, <span class="hljs-string">"POST"</span>).upper()

    count = <span class="hljs-number">0</span>
    <span class="hljs-keyword">for</span> row <span class="hljs-keyword">in</span> rows:
        count += <span class="hljs-number">1</span>
        <span class="hljs-comment"># Convert row to a dict for JSON</span>
        payload = row.asDict()
        <span class="hljs-keyword">if</span> method == <span class="hljs-string">"POST"</span>:
            resp = requests.post(url, json=payload)
        <span class="hljs-keyword">elif</span> method == <span class="hljs-string">"PUT"</span>:
            resp = requests.put(url, json=payload)
        <span class="hljs-keyword">else</span>:
            <span class="hljs-keyword">raise</span> NotImplementedError(<span class="hljs-string">f"Method <span class="hljs-subst">{method}</span> not supported."</span>)
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> resp.ok:
            <span class="hljs-keyword">raise</span> RuntimeError(
                <span class="hljs-string">f"Failed to write row <span class="hljs-subst">{payload}</span>, status: <span class="hljs-subst">{resp.status_code}</span>"</span>
            )

    <span class="hljs-keyword">return</span> SimpleCommitMessage(partition_id=partition_id, count=count)

<span class="hljs-keyword">def</span> <span class="hljs-title function_">commit</span>(<span class="hljs-params">self, messages: <span class="hljs-type">List</span>[SimpleCommitMessage]</span>) -&gt; <span class="hljs-literal">None</span>:
    total_count = <span class="hljs-built_in">sum</span>(m.count <span class="hljs-keyword">for</span> m <span class="hljs-keyword">in</span> messages)
    <span class="hljs-built_in">print</span>(<span class="hljs-string">f"SUCCESS: Wrote <span class="hljs-subst">{total_count}</span> rows to REST API."</span>)

<span class="hljs-keyword">def</span> <span class="hljs-title function_">abort</span>(<span class="hljs-params">self, messages: <span class="hljs-type">List</span>[SimpleCommitMessage]</span>) -&gt; <span class="hljs-literal">None</span>:
    <span class="hljs-built_in">print</span>(<span class="hljs-string">f"ABORT: Some tasks failed to write to the REST API."</span>)</pre></div><h1 id="f5d6">Using in Databricks</h1><p id="9887">To use your data source in Databricks notebook:</p><figure id="47f9"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*-KFHmiAxaqK64yNcEqge3A.png"><figcaption></figcaption></figure><h1 id="6e9c">Packaging as a Wheel</h1><p id="b74b">To share this connector with your team, you can <b>package</b> it as a wheel and upload it to volumes.</p><h1 id="f6c8">Conclusion</h1><p id="cb03">By <b>wrapping</b> REST logic into a custom Spark <b>Python Data Source</b>:</p><ul><li><b>Analysts</b> get a <b>single-line</b> interface to fetch data:</li></ul><figure id="2989"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*rzjsH5en9VsuUgD3d3whXA.png"><figcaption></figcaption></figure><ul><li><b>Engineers</b> standardize how REST calls happen (pagination, error handling, auth tokens) in <b>one place</b>.</li><li><b>Teams</b> can version and reuse the connector as a <b>wheel</b> in Databricks.</li></ul><p id="dcad">With Spark <b>Python Data Source API</b>, you can create your own connectors for essentially any REST or proprietary endpoint, unlocking consistent, streamlined data ingestion for your entire organization.</p><p id="f396"><b>Happy coding, and may your analysts never have to write requests code again!</b></p><p id="b20f"><b>The code used in that article can be downloaded from:</b></p><div id="c397" class="link-block">
      <a href="https://github.com/hubert-dudek/medium/tree/main/own-data-sources">
        <div>
          <div>
            <h2>medium/own-data-sources at main · hubert-dudek/medium</h2>
            <div><h3>Contribute to hubert-dudek/medium development by creating an account on GitHub.</h3></div>
            <div><p>github.com</p></div>
          </div>
          <div>
            <div style="background-image: url(https://miro.readmedium.com/v2/resize:fit:320/0*uWrjyTmKB9yyJOsO)"></div>
          </div>
        </div>
      </a>
    </div><figure id="5e5e"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/0*ONbYTljqthNRGEfQ.png"><figcaption>Hubert Dudek (author)</figcaption></figure><p id="b728"><i>If you like this blog post, consider buying me a coffee :-) <a href="https://ko-fi.com/hubertdudek">https://ko-fi.com/hubertdudek</a></i></p></article></body>

How to write own connector to Rest API in Spark Databricks

Access for free via a friend’s link https://databrickster.medium.com/how-to-write-own-connector-to-rest-api-in-spark-databricks-42321a5021dd?sk=89c9b3cf0c1d008d54c9d0bce73ac769

Imagine all a data engineer or analyst needs to do to read from a REST API is:

No direct requests calls, no manual JSON parsing — just Spark in databricks notebook. That’s the power of a custom Spark Data Source. In this article, we’ll explore how to build such a connector in Python using Spark’s newer Python Data Source API.

We’ll demonstrate using the JSONPlaceholder public API, which provides fake JSON data.

The Big Picture

In many organizations, analysts and data scientists often need to grab data from internal or external REST APIs. Typically, they end up writing boilerplate code:

This approach is repeated in multiple notebooks or pipelines — leading to duplication, inconsistency, and lots of maintenance headaches. By creating one custom connector, you:

  1. Hide the complexity of making HTTP requests.
  2. Standardize on a single approach for error handling, authentication, etc.
  3. Let anyone do a simple .format(“myrestdatasource”).option(“endpoint”, “…”).load() to access the data.

We can do this entirely in Python using the new Python Data Source API (already available in Databricks).

Quick Demo: How an Analyst Would Use It

Imagine you’ve created a package named myrestdatasource that does all the heavy lifting. Your analysts could then do the following:

Boom! They get a Spark DataFrame with columns like userId, id, title, and body.

No more repetitive code — just a consistent DataFrame interface.

Implementation: The Core Classes

Below is a minimal example of a data source named MyRestDataSource. For demonstration, we’ll use the public JSONPlaceholder API (a fake REST service).

Project structure:

rest_datasource.py:

import requests
from pyspark.sql.datasource import DataSource, DataSourceReader, DataSourceWriter, WriterCommitMessage
from pyspark.sql.types import (
    StructType,
    StructField,
    StringType,
    IntegerType
)
from typing import Iterator, List
from pyspark.sql.types import Row
from dataclasses import dataclass

# -----------------------------------------------------------------------------
# 1) Define a custom DataSource
# -----------------------------------------------------------------------------
class MyRestDataSource(DataSource):
    """
    A custom data source for reading (and optionally writing) data from a REST API.
    
    Example options:
    - endpoint: e.g. 'posts' => https://jsonplaceholder.typicode.com/posts
    - method: (for writes) e.g. 'POST', 'PUT'
    """

    @classmethod
    def name(cls):
        # The short name used in spark.read.format("myrestdatasource")
        return "myrestdatasource"

    def schema(self):
        """
        Return a schema string (or a StructType) that Spark can use.
        For this example, we assume a fixed schema for JSONPlaceholder 'posts':
          userId (int), id (int), title (string), body (string)
        """
        return "userId int, id int, title string, body string"

    def reader(self, schema: StructType):
        """
        Create and return a DataSourceReader for batch reads.
        """
        return MyRestDataSourceReader(schema, self.options)

    def writer(self, schema: StructType, overwrite: bool):
        """
        Create and return a DataSourceWriter for batch writes (if needed).
        """
        return MyRestDataSourceWriter(self.options, overwrite)


# -----------------------------------------------------------------------------
# 2) Define a DataSourceReader to handle reads
# -----------------------------------------------------------------------------
class MyRestDataSourceReader(DataSourceReader):
    def __init__(self, schema: StructType, options: dict):
        self.schema = schema
        # options is a dictionary of strings
        self.options = options

    def read(self, partition):
        """
        Called on each partition to return an iterator of rows.
        For simplicity, this example does NOT implement multiple partitions.
        """
        base_url = "https://jsonplaceholder.typicode.com"
        endpoint = self.options.get("endpoint", "posts")  # default to 'posts'
        url = f"{base_url}/{endpoint}"

        # Make a GET request
        resp = requests.get(url)
        resp.raise_for_status()
        data = resp.json()

        # data is a list of dicts (JSONPlaceholder format).
        # We yield tuples matching the schema [userId, id, title, body].
        for item in data:
            yield (
                item.get("userId"),
                item.get("id"),
                item.get("title"),
                item.get("body"),
            )

    def partitions(self):
        """
        If you want multiple partitions, you would define them here.
        For now, we'll return a single partition.
        """
        from pyspark.sql.datasource import InputPartition
        return [InputPartition(0)]


# -----------------------------------------------------------------------------
# 3) (Optional) Define a DataSourceWriter to handle writes
# -----------------------------------------------------------------------------
@dataclass
class SimpleCommitMessage(WriterCommitMessage):
    partition_id: int
    count: int

class MyRestDataSourceWriter(DataSourceWriter):
    """
    This is a minimal example of writing to a REST API.
    JSONPlaceholder won't actually persist the data, but let's illustrate.
    """

    def __init__(self, options: dict, overwrite: bool):
        self.options = options
        self.overwrite = overwrite

    def write(self, rows: Iterator[Row]) -> WriterCommitMessage:
        """
        Called on each partition to write data. 
        Return a commit message for the partition.
        """
        from pyspark import TaskContext
        context = TaskContext.get()
        partition_id = context.partitionId()

        base_url = "https://jsonplaceholder.typicode.com"
        endpoint = self.options.get("endpoint", "posts")
        url = f"{base_url}/{endpoint}"
        method = self.options.get("method", "POST").upper()

        count = 0
        for row in rows:
            count += 1
            # Convert row to a dict for JSON
            payload = row.asDict()
            if method == "POST":
                resp = requests.post(url, json=payload)
            elif method == "PUT":
                resp = requests.put(url, json=payload)
            else:
                raise NotImplementedError(f"Method {method} not supported.")
            if not resp.ok:
                raise RuntimeError(
                    f"Failed to write row {payload}, status: {resp.status_code}"
                )

        return SimpleCommitMessage(partition_id=partition_id, count=count)

    def commit(self, messages: List[SimpleCommitMessage]) -> None:
        total_count = sum(m.count for m in messages)
        print(f"SUCCESS: Wrote {total_count} rows to REST API.")

    def abort(self, messages: List[SimpleCommitMessage]) -> None:
        print(f"ABORT: Some tasks failed to write to the REST API.")

Using in Databricks

To use your data source in Databricks notebook:

Packaging as a Wheel

To share this connector with your team, you can package it as a wheel and upload it to volumes.

Conclusion

By wrapping REST logic into a custom Spark Python Data Source:

  • Analysts get a single-line interface to fetch data:
  • Engineers standardize how REST calls happen (pagination, error handling, auth tokens) in one place.
  • Teams can version and reuse the connector as a wheel in Databricks.

With Spark Python Data Source API, you can create your own connectors for essentially any REST or proprietary endpoint, unlocking consistent, streamlined data ingestion for your entire organization.

Happy coding, and may your analysts never have to write requests code again!

The code used in that article can be downloaded from:

Hubert Dudek (author)

If you like this blog post, consider buying me a coffee :-) https://ko-fi.com/hubertdudek

Recommended from ReadMedium