Client Libraries & Examples
Client Libraries & Examples
Official and community client libraries for the Bloqr Compiler API.
Official Clients
Python
Modern async client using httpx with full type annotations.
from __future__ import annotations
import httpxfrom dataclasses import dataclassfrom typing import AsyncIterator, Iteratorfrom collections.abc import Callable
@dataclassclass Source: """Filter list source configuration.""" source: str name: str | None = None type: str | None = None # 'adblock' or 'hosts' transformations: list[str] | None = None
@dataclassclass CompileResult: """Compilation result with metrics.""" success: bool rules: list[str] rule_count: int cached: bool = False metrics: dict | None = None error: str | None = None
class BloqrCompilerError(Exception): """Raised when compilation fails.""" pass
class BloqrCompiler: """Modern async/sync Python client for Bloqr Compiler API."""
DEFAULT_URL = "https://api.bloqr.dev" DEFAULT_TRANSFORMS = ["Deduplicate", "RemoveEmptyLines"]
def __init__( self, base_url: str = DEFAULT_URL, timeout: float = 30.0, max_retries: int = 3, ) -> None: self.base_url = base_url.rstrip("/") self.timeout = timeout self.max_retries = max_retries
def _build_payload( self, sources: list[Source | dict], name: str, transformations: list[str] | None, benchmark: bool, ) -> dict: source_list = [ s if isinstance(s, dict) else { "source": s.source, "name": s.name, "type": s.type, "transformations": s.transformations, } for s in sources ] return { "configuration": { "name": name, "sources": source_list, "transformations": transformations or self.DEFAULT_TRANSFORMS, }, "benchmark": benchmark, }
def _parse_result(self, data: dict) -> CompileResult: if not data.get("success", False): raise BloqrCompilerError(data.get("error", "Unknown error")) return CompileResult( success=True, rules=data.get("rules", []), rule_count=data.get("ruleCount", 0), cached=data.get("cached", False), metrics=data.get("metrics"), )
def compile( self, sources: list[Source | dict], name: str = "Compiled List", transformations: list[str] | None = None, benchmark: bool = False, ) -> CompileResult: """Synchronous compilation.""" payload = self._build_payload(sources, name, transformations, benchmark)
transport = httpx.HTTPTransport(retries=self.max_retries) with httpx.Client(transport=transport, timeout=self.timeout) as client: response = client.post( f"{self.base_url}/compile", json=payload, headers={"Content-Type": "application/json"}, ) response.raise_for_status() return self._parse_result(response.json())
async def compile_async( self, sources: list[Source | dict], name: str = "Compiled List", transformations: list[str] | None = None, benchmark: bool = False, ) -> CompileResult: """Asynchronous compilation.""" payload = self._build_payload(sources, name, transformations, benchmark)
transport = httpx.AsyncHTTPTransport(retries=self.max_retries) async with httpx.AsyncClient(transport=transport, timeout=self.timeout) as client: response = await client.post( f"{self.base_url}/compile", json=payload, headers={"Content-Type": "application/json"}, ) response.raise_for_status() return self._parse_result(response.json())
def compile_stream( self, sources: list[Source | dict], name: str = "Compiled List", transformations: list[str] | None = None, on_event: Callable[[str, dict], None] | None = None, ) -> Iterator[tuple[str, dict]]: """Stream compilation events using SSE.""" payload = self._build_payload(sources, name, transformations, benchmark=False)
with httpx.Client(timeout=None) as client: with client.stream( "POST", f"{self.base_url}/compile/stream", json=payload, headers={"Content-Type": "application/json"}, ) as response: response.raise_for_status() event_type = ""
for line in response.iter_lines(): if line.startswith("event: "): event_type = line[7:] elif line.startswith("data: "): import json data = json.loads(line[6:]) if on_event: on_event(event_type, data) yield event_type, data
async def compile_stream_async( self, sources: list[Source | dict], name: str = "Compiled List", transformations: list[str] | None = None, ) -> AsyncIterator[tuple[str, dict]]: """Async stream compilation events using SSE.""" payload = self._build_payload(sources, name, transformations, benchmark=False)
async with httpx.AsyncClient(timeout=None) as client: async with client.stream( "POST", f"{self.base_url}/compile/stream", json=payload, headers={"Content-Type": "application/json"}, ) as response: response.raise_for_status() event_type = ""
async for line in response.aiter_lines(): if line.startswith("event: "): event_type = line[7:] elif line.startswith("data: "): import json data = json.loads(line[6:]) yield event_type, data
# Example usageif __name__ == "__main__": import asyncio
client = BloqrCompiler()
# Synchronous compilation result = client.compile( sources=[Source(source="https://easylist.to/easylist/easylist.txt")], name="My Filter List", benchmark=True, ) print(f"Compiled {result.rule_count} rules") if result.metrics: print(f"Duration: {result.metrics['totalDurationMs']}ms")
# Async compilation async def main(): result = await client.compile_async( sources=[{"source": "https://easylist.to/easylist/easylist.txt"}], benchmark=True, ) print(f"Async compiled {result.rule_count} rules")
# Async streaming async for event_type, data in client.compile_stream_async( sources=[{"source": "https://easylist.to/easylist/easylist.txt"}], ): if event_type == "progress": print(f"Progress: {data.get('message')}") elif event_type == "result": print(f"Complete! {data['ruleCount']} rules")
asyncio.run(main())JavaScript/TypeScript
Modern TypeScript client with retry logic, AbortController support, and custom error handling.
// Typesinterface Source { source: string; name?: string; type?: 'adblock' | 'hosts'; transformations?: string[];}
interface CompileOptions { name?: string; transformations?: string[]; benchmark?: boolean; signal?: AbortSignal;}
interface CompileResult { success: boolean; rules: string[]; ruleCount: number; cached: boolean; metrics?: { totalDurationMs: number; sourceCount: number; ruleCount: number; };}
interface StreamEvent { event: 'progress' | 'result' | 'error'; data: Record<string, unknown>;}
// Custom errorsclass BloqrCompilerError extends Error { constructor( message: string, public readonly statusCode?: number, public readonly retryAfter?: number, ) { super(message); this.name = 'BloqrCompilerError'; }}
class RateLimitError extends BloqrCompilerError { constructor(retryAfter: number) { super(`Rate limited. Retry after ${retryAfter}s`, 429, retryAfter); this.name = 'RateLimitError'; }}
// Clientclass BloqrCompiler { private readonly baseUrl: string; private readonly maxRetries: number; private readonly retryDelayMs: number;
static readonly DEFAULT_URL = 'https://api.bloqr.dev'; static readonly DEFAULT_TRANSFORMS = ['Deduplicate', 'RemoveEmptyLines'];
constructor(options: { baseUrl?: string; maxRetries?: number; retryDelayMs?: number; } = {}) { this.baseUrl = options.baseUrl?.replace(/\/$/, '') ?? BloqrCompiler.DEFAULT_URL; this.maxRetries = options.maxRetries ?? 3; this.retryDelayMs = options.retryDelayMs ?? 1000; }
private async fetchWithRetry( url: string, init: RequestInit, retries = this.maxRetries, ): Promise<Response> { let lastError: Error | undefined;
for (let attempt = 0; attempt <= retries; attempt++) { try { const response = await fetch(url, init);
if (response.status === 429) { const retryAfter = parseInt(response.headers.get('Retry-After') ?? '60', 10); throw new RateLimitError(retryAfter); }
if (!response.ok) { throw new BloqrCompilerError( `HTTP ${response.status}: ${response.statusText}`, response.status, ); }
return response; } catch (error) { lastError = error as Error;
// Don't retry on rate limits or abort if (error instanceof RateLimitError) throw error; if (init.signal?.aborted) throw error;
// Retry on network errors if (attempt < retries) { await new Promise(r => setTimeout(r, this.retryDelayMs * (attempt + 1))); } } }
throw lastError; }
async compile(sources: Source[], options: CompileOptions = {}): Promise<CompileResult> { const payload = { configuration: { name: options.name ?? 'Compiled List', sources, transformations: options.transformations ?? BloqrCompiler.DEFAULT_TRANSFORMS, }, benchmark: options.benchmark ?? false, };
const response = await this.fetchWithRetry( `${this.baseUrl}/compile`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: options.signal, }, );
const result = await response.json();
if (!result.success) { throw new BloqrCompilerError(`Compilation failed: ${result.error}`); }
return result; }
async *compileStream( sources: Source[], options: Omit<CompileOptions, 'benchmark'> = {}, ): AsyncGenerator<StreamEvent> { const payload = { configuration: { name: options.name ?? 'Compiled List', sources, transformations: options.transformations ?? BloqrCompiler.DEFAULT_TRANSFORMS, }, };
const response = await this.fetchWithRetry( `${this.baseUrl}/compile/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: options.signal, }, );
const reader = response.body!.getReader(); const decoder = new TextDecoder(); let buffer = ''; let currentEvent = '';
try { while (true) { const { done, value } = await reader.read(); if (done) break;
buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() ?? '';
for (const line of lines) { if (line.startsWith('event: ')) { currentEvent = line.slice(7); } else if (line.startsWith('data: ')) { yield { event: currentEvent as StreamEvent['event'], data: JSON.parse(line.slice(6)), }; } } } } finally { reader.releaseLock(); } }}
// Example usageconst client = new BloqrCompiler({ maxRetries: 3 });
// With AbortController for cancellationconst controller = new AbortController();setTimeout(() => controller.abort(), 30000); // 30s timeout
try { const result = await client.compile( [{ source: 'https://easylist.to/easylist/easylist.txt' }], { name: 'My Filter List', benchmark: true, signal: controller.signal, }, );
console.log(`Compiled ${result.ruleCount} rules`); console.log(`Duration: ${result.metrics?.totalDurationMs}ms`); console.log(`Cached: ${result.cached}`);} catch (error) { if (error instanceof RateLimitError) { console.log(`Rate limited. Retry after ${error.retryAfter}s`); } else { throw error; }}
// Streaming with progress updatesfor await (const { event, data } of client.compileStream([ { source: 'https://easylist.to/easylist/easylist.txt' },])) { switch (event) { case 'progress': console.log(`Progress: ${data.message}`); break; case 'result': console.log(`Complete! ${data.ruleCount} rules`); break; case 'error': console.error(`Error: ${data.message}`); break; }}Go
Modern Go client with context support, retry logic, and proper error handling.
package adblock
import ( "bufio" "bytes" "context" "encoding/json" "errors" "fmt" "net/http" "strconv" "strings" "time")
const ( DefaultBaseURL = "https://api.bloqr.dev" DefaultTimeout = 30 * time.Second DefaultMaxRetries = 3)
var ( ErrRateLimited = errors.New("rate limited") ErrCompilationFailed = errors.New("compilation failed"))
// Source represents a filter list source.type Source struct { Source string `json:"source"` Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` Transformations []string `json:"transformations,omitempty"`}
// Metrics contains compilation performance metrics.type Metrics struct { TotalDurationMs int `json:"totalDurationMs"` SourceCount int `json:"sourceCount"` RuleCount int `json:"ruleCount"`}
// CompileResult represents the compilation response.type CompileResult struct { Success bool `json:"success"` Rules []string `json:"rules"` RuleCount int `json:"ruleCount"` Cached bool `json:"cached"` Metrics *Metrics `json:"metrics,omitempty"` Error string `json:"error,omitempty"`}
// Event represents a Server-Sent Event from streaming compilation.type Event struct { Type string Data map[string]any}
// CompileOptions configures a compilation request.type CompileOptions struct { Name string Transformations []string Benchmark bool}
// Compiler is the Bloqr Compiler API client.type Compiler struct { baseURL string client *http.Client maxRetries int}
// Option configures a Compiler.type Option func(*Compiler)
// WithBaseURL sets a custom API base URL.func WithBaseURL(url string) Option { return func(c *Compiler) { c.baseURL = strings.TrimRight(url, "/") }}
// WithTimeout sets the HTTP client timeout.func WithTimeout(d time.Duration) Option { return func(c *Compiler) { c.client.Timeout = d }}
// WithMaxRetries sets the maximum retry attempts.func WithMaxRetries(n int) Option { return func(c *Compiler) { c.maxRetries = n }}
// NewCompiler creates a new Bloqr Compiler client.func NewCompiler(opts ...Option) *Compiler { c := &Compiler{ baseURL: DefaultBaseURL, client: &http.Client{Timeout: DefaultTimeout}, maxRetries: DefaultMaxRetries, } for _, opt := range opts { opt(c) } return c}
func (c *Compiler) doWithRetry(ctx context.Context, req *http.Request) (*http.Response, error) { var lastErr error
for attempt := 0; attempt <= c.maxRetries; attempt++ { if attempt > 0 { select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(time.Duration(attempt) * time.Second): } }
resp, err := c.client.Do(req.WithContext(ctx)) if err != nil { lastErr = err continue }
if resp.StatusCode == http.StatusTooManyRequests { resp.Body.Close() retryAfter, _ := strconv.Atoi(resp.Header.Get("Retry-After")) lastErr = fmt.Errorf("%w: retry after %ds", ErrRateLimited, retryAfter) continue }
if resp.StatusCode >= 500 { resp.Body.Close() lastErr = fmt.Errorf("server error: %s", resp.Status) continue }
return resp, nil }
return nil, lastErr}
// Compile compiles filter lists and returns the result.func (c *Compiler) Compile(ctx context.Context, sources []Source, opts *CompileOptions) (*CompileResult, error) { if opts == nil { opts = &CompileOptions{} } if opts.Name == "" { opts.Name = "Compiled List" } if opts.Transformations == nil { opts.Transformations = []string{"Deduplicate", "RemoveEmptyLines"} }
payload := map[string]any{ "configuration": map[string]any{ "name": opts.Name, "sources": sources, "transformations": opts.Transformations, }, "benchmark": opts.Benchmark, }
body, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) }
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/compile", bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json")
resp, err := c.doWithRetry(ctx, req) if err != nil { return nil, err } defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status: %s", resp.Status) }
var result CompileResult if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("decode response: %w", err) }
if !result.Success { return nil, fmt.Errorf("%w: %s", ErrCompilationFailed, result.Error) }
return &result, nil}
// CompileStream compiles filter lists and streams events via a channel.// The returned channel is closed when the stream ends or context is canceled.func (c *Compiler) CompileStream(ctx context.Context, sources []Source, opts *CompileOptions) (<-chan Event, <-chan error) { events := make(chan Event) errc := make(chan error, 1)
go func() { defer close(events) defer close(errc)
if opts == nil { opts = &CompileOptions{} } if opts.Name == "" { opts.Name = "Compiled List" } if opts.Transformations == nil { opts.Transformations = []string{"Deduplicate", "RemoveEmptyLines"} }
payload := map[string]any{ "configuration": map[string]any{ "name": opts.Name, "sources": sources, "transformations": opts.Transformations, }, }
body, err := json.Marshal(payload) if err != nil { errc <- fmt.Errorf("marshal request: %w", err) return }
req, err := http.NewRequest(http.MethodPost, c.baseURL+"/compile/stream", bytes.NewReader(body)) if err != nil { errc <- fmt.Errorf("create request: %w", err) return } req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req.WithContext(ctx)) if err != nil { errc <- err return } defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { errc <- fmt.Errorf("unexpected status: %s", resp.Status) return }
scanner := bufio.NewScanner(resp.Body) var eventType string
for scanner.Scan() { select { case <-ctx.Done(): errc <- ctx.Err() return default: }
line := scanner.Text() switch { case strings.HasPrefix(line, "event: "): eventType = strings.TrimPrefix(line, "event: ") case strings.HasPrefix(line, "data: "): var data map[string]any if err := json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &data); err == nil { events <- Event{Type: eventType, Data: data} } } }
if err := scanner.Err(); err != nil { errc <- err } }()
return events, errc}
// Example usagefunc main() { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel()
client := NewCompiler( WithMaxRetries(3), WithTimeout(30*time.Second), )
// Simple compilation result, err := client.Compile(ctx, []Source{ {Source: "https://easylist.to/easylist/easylist.txt"}, }, &CompileOptions{ Name: "My Filter List", Benchmark: true, }) if err != nil { if errors.Is(err, ErrRateLimited) { fmt.Println("Rate limited, try again later") return } panic(err) }
fmt.Printf("Compiled %d rules", result.RuleCount) if result.Metrics != nil { fmt.Printf(" in %dms", result.Metrics.TotalDurationMs) } fmt.Printf(" (cached: %v)\n", result.Cached)
// Streaming compilation events, errc := client.CompileStream(ctx, []Source{ {Source: "https://easylist.to/easylist/easylist.txt"}, }, nil)
for event := range events { switch event.Type { case "progress": fmt.Printf("Progress: %v\n", event.Data["message"]) case "result": fmt.Printf("Complete! %v rules\n", event.Data["ruleCount"]) case "error": fmt.Printf("Error: %v\n", event.Data["message"]) } }
if err := <-errc; err != nil { fmt.Printf("Stream error: %v\n", err) }}Rust
Async Rust client using reqwest and tokio.
use reqwest::{Client, StatusCode};use serde::{Deserialize, Serialize};use std::time::Duration;use thiserror::Error;
const DEFAULT_BASE_URL: &str = "https://api.bloqr.dev";
#[derive(Error, Debug)]pub enum AdblockError { #[error("HTTP error: {0}")] Http(#[from] reqwest::Error), #[error("Rate limited, retry after {0}s")] RateLimited(u64), #[error("Compilation failed: {0}")] CompilationFailed(String), #[error("Parse error: {0}")] Parse(#[from] serde_json::Error),}
#[derive(Debug, Clone, Serialize)]pub struct Source { pub source: String, #[serde(skip_serializing_if = "Option::is_none")] pub name: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub r#type: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub transformations: Option<Vec<String>>,}
impl Source { pub fn new(source: impl Into<String>) -> Self { Self { source: source.into(), name: None, r#type: None, transformations: None, } }}
#[derive(Debug, Clone, Deserialize)]#[serde(rename_all = "camelCase")]pub struct Metrics { pub total_duration_ms: u64, pub source_count: usize, pub rule_count: usize,}
#[derive(Debug, Clone, Deserialize)]#[serde(rename_all = "camelCase")]pub struct CompileResult { pub success: bool, pub rules: Vec<String>, pub rule_count: usize, #[serde(default)] pub cached: bool, pub metrics: Option<Metrics>, pub error: Option<String>,}
#[derive(Debug, Clone, Serialize)]struct CompileRequest { configuration: Configuration, benchmark: bool,}
#[derive(Debug, Clone, Serialize)]struct Configuration { name: String, sources: Vec<Source>, transformations: Vec<String>,}
pub struct BloqrCompiler { client: Client, base_url: String, max_retries: u32,}
impl Default for BloqrCompiler { fn default() -> Self { Self::new() }}
impl BloqrCompiler { pub fn new() -> Self { Self { client: Client::builder() .timeout(Duration::from_secs(30)) .build() .expect("Failed to create HTTP client"), base_url: DEFAULT_BASE_URL.to_string(), max_retries: 3, } }
pub fn with_base_url(mut self, url: impl Into<String>) -> Self { self.base_url = url.into().trim_end_matches('/').to_string(); self }
pub fn with_timeout(mut self, timeout: Duration) -> Self { self.client = Client::builder() .timeout(timeout) .build() .expect("Failed to create HTTP client"); self }
pub fn with_max_retries(mut self, retries: u32) -> Self { self.max_retries = retries; self }
pub async fn compile( &self, sources: Vec<Source>, name: Option<&str>, transformations: Option<Vec<String>>, benchmark: bool, ) -> Result<CompileResult, AdblockError> { let request = CompileRequest { configuration: Configuration { name: name.unwrap_or("Compiled List").to_string(), sources, transformations: transformations .unwrap_or_else(|| vec!["Deduplicate".into(), "RemoveEmptyLines".into()]), }, benchmark, };
let mut last_error = None;
for attempt in 0..=self.max_retries { if attempt > 0 { tokio::time::sleep(Duration::from_secs(attempt as u64)).await; }
let response = match self .client .post(format!("{}/compile", self.base_url)) .json(&request) .send() .await { Ok(resp) => resp, Err(e) => { last_error = Some(AdblockError::Http(e)); continue; } };
match response.status() { StatusCode::TOO_MANY_REQUESTS => { let retry_after = response .headers() .get("Retry-After") .and_then(|v| v.to_str().ok()) .and_then(|v| v.parse().ok()) .unwrap_or(60); last_error = Some(AdblockError::RateLimited(retry_after)); continue; } status if status.is_server_error() => { last_error = Some(AdblockError::CompilationFailed(format!( "Server error: {}", status ))); continue; } _ => {} }
let result: CompileResult = response.json().await?;
if !result.success { return Err(AdblockError::CompilationFailed( result.error.unwrap_or_else(|| "Unknown error".to_string()), )); }
return Ok(result); }
Err(last_error.unwrap_or_else(|| AdblockError::CompilationFailed("Max retries exceeded".to_string()))) }}
// Example usage#[tokio::main]async fn main() -> Result<(), AdblockError> { let client = BloqrCompiler::new() .with_max_retries(3) .with_timeout(Duration::from_secs(60));
let result = client .compile( vec![Source::new("https://easylist.to/easylist/easylist.txt")], Some("My Filter List"), None, true, ) .await?;
println!("Compiled {} rules", result.rule_count); if let Some(metrics) = &result.metrics { println!("Duration: {}ms", metrics.total_duration_ms); } println!("Cached: {}", result.cached);
Ok(())}C# / .NET
Modern C# client using HttpClient and async/await patterns.
using System.Net;using System.Net.Http.Json;using System.Runtime.CompilerServices;using System.Text.Json;using System.Text.Json.Serialization;
namespace BloqrCompiler;
public record Source( [property: JsonPropertyName("source")] string Url, [property: JsonPropertyName("name")] string? Name = null, [property: JsonPropertyName("type")] string? Type = null, [property: JsonPropertyName("transformations")] List<string>? Transformations = null);
public record Metrics( [property: JsonPropertyName("totalDurationMs")] int TotalDurationMs, [property: JsonPropertyName("sourceCount")] int SourceCount, [property: JsonPropertyName("ruleCount")] int RuleCount);
public record CompileResult( [property: JsonPropertyName("success")] bool Success, [property: JsonPropertyName("rules")] List<string> Rules, [property: JsonPropertyName("ruleCount")] int RuleCount, [property: JsonPropertyName("cached")] bool Cached = false, [property: JsonPropertyName("metrics")] Metrics? Metrics = null, [property: JsonPropertyName("error")] string? Error = null);
public record StreamEvent(string EventType, JsonElement Data);
public class BloqrCompilerException : Exception{ public HttpStatusCode? StatusCode { get; } public int? RetryAfter { get; }
public BloqrCompilerException(string message, HttpStatusCode? statusCode = null, int? retryAfter = null) : base(message) { StatusCode = statusCode; RetryAfter = retryAfter; }}
public class RateLimitException : BloqrCompilerException{ public RateLimitException(int retryAfter) : base($"Rate limited. Retry after {retryAfter}s", HttpStatusCode.TooManyRequests, retryAfter) { }}
public sealed class BloqrCompilerClient : IDisposable{ private const string DefaultBaseUrl = "https://api.bloqr.dev"; private static readonly string[] DefaultTransformations = ["Deduplicate", "RemoveEmptyLines"];
private readonly HttpClient _httpClient; private readonly string _baseUrl; private readonly int _maxRetries;
public BloqrCompilerClient( string? baseUrl = null, TimeSpan? timeout = null, int maxRetries = 3) { _baseUrl = (baseUrl ?? DefaultBaseUrl).TrimEnd('/'); _maxRetries = maxRetries; _httpClient = new HttpClient { Timeout = timeout ?? TimeSpan.FromSeconds(30) }; }
public async Task<CompileResult> CompileAsync( IEnumerable<Source> sources, string? name = null, IEnumerable<string>? transformations = null, bool benchmark = false, CancellationToken cancellationToken = default) { var request = new { configuration = new { name = name ?? "Compiled List", sources = sources.ToList(), transformations = transformations?.ToList() ?? DefaultTransformations.ToList() }, benchmark };
Exception? lastException = null;
for (var attempt = 0; attempt <= _maxRetries; attempt++) { if (attempt > 0) { await Task.Delay(TimeSpan.FromSeconds(attempt), cancellationToken); }
try { var response = await _httpClient.PostAsJsonAsync( $"{_baseUrl}/compile", request, cancellationToken);
if (response.StatusCode == HttpStatusCode.TooManyRequests) { var retryAfter = int.TryParse( response.Headers.GetValues("Retry-After").FirstOrDefault(), out var ra) ? ra : 60; throw new RateLimitException(retryAfter); }
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<CompileResult>(cancellationToken) ?? throw new BloqrCompilerException("Failed to deserialize response");
if (!result.Success) { throw new BloqrCompilerException($"Compilation failed: {result.Error}"); }
return result; } catch (RateLimitException) { throw; } catch (OperationCanceledException) { throw; } catch (Exception ex) { lastException = ex; } }
throw lastException ?? new BloqrCompilerException("Max retries exceeded"); }
public async IAsyncEnumerable<StreamEvent> CompileStreamAsync( IEnumerable<Source> sources, string? name = null, IEnumerable<string>? transformations = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var request = new { configuration = new { name = name ?? "Compiled List", sources = sources.ToList(), transformations = transformations?.ToList() ?? DefaultTransformations.ToList() } };
var response = await _httpClient.PostAsJsonAsync( $"{_baseUrl}/compile/stream", request, cancellationToken);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); using var reader = new StreamReader(stream);
var currentEvent = "";
while (!reader.EndOfStream) { cancellationToken.ThrowIfCancellationRequested();
var line = await reader.ReadLineAsync(cancellationToken); if (string.IsNullOrEmpty(line)) continue;
if (line.StartsWith("event: ")) { currentEvent = line[7..]; } else if (line.StartsWith("data: ")) { var data = JsonSerializer.Deserialize<JsonElement>(line[6..]); yield return new StreamEvent(currentEvent, data); } } }
public void Dispose() => _httpClient.Dispose();}
// Example usagepublic static class Program{ public static async Task Main() { using var client = new BloqrCompilerClient( timeout: TimeSpan.FromSeconds(60), maxRetries: 3);
try { // Simple compilation var result = await client.CompileAsync( sources: [new Source("https://easylist.to/easylist/easylist.txt")], name: "My Filter List", benchmark: true);
Console.WriteLine($"Compiled {result.RuleCount} rules"); if (result.Metrics is not null) { Console.WriteLine($"Duration: {result.Metrics.TotalDurationMs}ms"); } Console.WriteLine($"Cached: {result.Cached}");
// Streaming compilation await foreach (var evt in client.CompileStreamAsync( sources: [new Source("https://easylist.to/easylist/easylist.txt")])) { switch (evt.EventType) { case "progress": Console.WriteLine($"Progress: {evt.Data.GetProperty("message")}"); break; case "result": Console.WriteLine($"Complete! {evt.Data.GetProperty("ruleCount")} rules"); break; case "error": Console.WriteLine($"Error: {evt.Data.GetProperty("message")}"); break; } } } catch (RateLimitException ex) { Console.WriteLine($"Rate limited. Retry after {ex.RetryAfter}s"); } }}Community Clients
Contributions welcome for additional language support:
- Ruby
- PHP
- Java
- Swift
- Kotlin
Installation
Python
pip install httpx # Modern async HTTP client# Save the client code as bloqr_compiler.pyJavaScript/TypeScript
# No dependencies required - uses native fetch# Works in Node.js 18+, Deno, Bun, and all modern browsersGo
go get # No external dependencies - uses standard library# Save as adblock/compiler.goRust
# Add to Cargo.toml[dependencies]reqwest = { version = "0.12", features = ["json"] }serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"thiserror = "2.0"tokio = { version = "1", features = ["full"] }C# / .NET
# .NET 8+ required (uses native JSON and HTTP support)dotnet new console# No additional packages neededError Handling
All clients handle the following errors:
- 429 Too Many Requests: Rate limit exceeded (max 10 req/min)
- 400 Bad Request: Invalid configuration
- 500 Internal Server Error: Compilation failed
Caching
The API automatically caches compilation results for 1 hour. Check the X-Cache header:
HIT: Result served from cacheMISS: Fresh compilation
Rate Limiting
- Limit: 10 requests per minute per IP
- Window: 60 seconds (sliding)
- Response: HTTP 429 with
Retry-Afterheader
Support
- GitHub: bloqr-systems/bloqr-landing
- Issues: Submit a bug report
- API Docs: docs/api/README.md
Bloqr AI™ — The privacy you didn't know you needed.
© 2026 Bloqr AI™, a trademark of Bloqr Systems™. Created by Bloqr Systems™, founded by Jayson Knight.
Internet Hygiene (n.) — the ongoing practices that keep your digital life clean, private, and safe.
Our product repos live in the BloqrAI org, part of the Bloqr Systems GitHub Enterprise. Product repos are internal-visibility — enterprise membership is required to view them.