agoor97's picture
Add application files
9e22989
# Add SQLite fix for non-Windows platforms
import platform
if platform.system() != "Windows": # Skip on Windows
__import__('pysqlite3')
import sys
sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')
import streamlit as st
from datetime import datetime
import time
import plotly.express as px
import pandas as pd
from src.utils.config import *
from src.utils.helper import *
from src.utils.styles import *
from src.db_agent import VarturRealEstateSearch
from src.analytics import RealEstateAnalytics
class EnhancedStreamlitApp:
def __init__(self):
self.config = AppConfig()
init_folders()
# Initialize the search system only once and store it in session state
if 'search_system' not in st.session_state:
st.session_state.search_system = VarturRealEstateSearch(persist_directory=DB_FOLDER)
# Assign the search system from session state
self.search_system = st.session_state.search_system
def render_login(self):
st.markdown("""
<div style='text-align: center; padding: 50px;'>
<h1>๐Ÿข VARTUR ยฎ Real Estate Brokerage</h1>
<p>Advanced Property Search Engine</p>
</div>
""", unsafe_allow_html=True)
with st.form("login_form", clear_on_submit=True):
st.markdown("<h3 style='text-align: center;'>Welcome Back</h3>", unsafe_allow_html=True)
password = st.text_input("Password", type="password")
if st.form_submit_button("Login", use_container_width=True):
if self.config.verify_password(password):
st.session_state.authenticated = True
st.rerun()
else:
st.error("โŒ Invalid password")
def render_dashboard(self):
stats = self.config.get_stats()
total_properties = self.search_system.collection.count()
col1, col2, col3 = st.columns(3)
with col1:
st.markdown(f"""
<div class='stats-card'>
<h3>๐Ÿ“Š Indexed Files</h3>
<h2>{stats["total_files"]}</h2>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown(f"""
<div class='stats-card'>
<h3>๐Ÿข Total Properties</h3>
<h2>{total_properties}</h2>
</div>
""", unsafe_allow_html=True)
with col3:
st.markdown(f"""
<div class='stats-card'>
<h3>๐Ÿ”„ Last Updated</h3>
<div style="display: inline-flex; align-items: baseline; gap: 0px;">
<h2 style="margin: 0;">
{datetime.fromisoformat(stats["last_updated"]).astimezone(DUBAI_TZ).strftime("%d %b, %H:%M") if stats["last_updated"] else "Never"}
</h2>
<span style="font-size: 1.0em; opacity: 0.7;">(Dubai TZ)</span>
</div>
</div>
""", unsafe_allow_html=True)
def render_search(self):
"""Render the search interface with filters"""
st.markdown("""
<div style='background: linear-gradient(120deg, #1a5f7a 0%, #66a6ff 100%);
padding: 15px;
border-radius: 10px;
margin-bottom: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);'>
<h3 style='color: white; margin: 0;'>๐Ÿ” Search</h3>
<p style='color: #e0e0e0; margin: 5px 0 0 0;'>search with your custom needs</p>
</div>
""", unsafe_allow_html=True)
# reference information box
with st.expander("๐Ÿ“‹ Search Functionality Guidelines", expanded=False):
st.markdown("""
##### ๐Ÿ“Œ Search System Overview
The search functionality works in two primary ways:
**1. ๐Ÿ—ฃ๏ธ Natural Language Search Box**
- Located at the top of the page
- Useful for broad, descriptive searches
- Examples: "sea view apartments", "large penthouses"
- Note: As this searches structured data, results may vary
**2. โšก Filter System**
- More precise filtering options
- Exact matching on specific criteria
- Filters applied after text search
- More reliable for specific requirements
**๐Ÿ’ก Best Practices**
- For exact requirements: Use filters
- For exploration: Use text search
- For specific items with context: Combine both
- Filters always take precedence over text search
**๐Ÿ”— Power of Combinations:**
- Combine filters for precise results:
* "Penthouse" search + Sea view + Price 2-5M + High floor
* 4BR + Burj view + EMAAR + 2000-3000 sq.ft
* Downtown area + 2BR + Floor 10+ + Price up to 2M
- Mix and match any number of filters to narrow down exactly what you want
- The more filters you combine, the more specific your results
""")
# Search Configuration
config_col1, config_col2 = st.columns([3, 1])
with config_col1:
query = st.text_input(
"๐Ÿ” Search Properties",
value="",
placeholder="Type Naturally: e.g. 4 bedroom with sea view, 2br downtown, penthouse burj view",
help="Search is flexible - Try different ways of describing what you want",
)
with config_col2:
n_results = st.number_input(
"๐Ÿ“Š Results Limit",
min_value=1,
max_value=1000,
value=10,
help="Maximum number of results"
)
# Organize filters in tabs
filter_tabs = st.tabs(["๐Ÿ“ Basic Filters", "โš™๏ธ Advanced Filters"])
with filter_tabs[0]:
col1, col2 = st.columns(2)
# Column 1: Price and Unit Type
with col1:
price_col1, price_col2 = st.columns(2)
with price_col1:
min_price = st.number_input(
"๐Ÿ’ฐ Min Price (M)",
value=0.0,
step=0.1,
format="%.1f",
help="Min Price in millions of AED",
)
with price_col2:
max_price = st.number_input(
"๐Ÿ’ฐ Max Price (M)",
value=0.0,
step=0.1,
format="%.1f",
help="Max Price in millions of AED",
)
unit_type = st.text_input(
"๐Ÿ  Unit Type",
placeholder="e.g. 4BR, 2 bed, studio, TH...",
help="Flexible input: '4BR', '2 bed', 'studio', etc."
)
# Column 2: Developer and View
with col2:
developer = st.text_input(
"๐Ÿ—๏ธ Developer",
placeholder="Type developer name",
help="Case-insensitive search"
)
view = st.text_input(
"๐Ÿ‘๏ธ View",
placeholder="e.g. sea, burj...",
help="Type any part of the view"
)
with filter_tabs[1]:
col1, col2 = st.columns(2)
# Column 1: Area Range
with col1:
area_col1, area_col2 = st.columns(2)
with area_col1:
min_area = st.number_input(
"๐Ÿ“ Min Area (sq.ft)",
value=0,
step=100,
help="Min Area in square feet",
)
with area_col2:
max_area = st.number_input(
"๐Ÿ“ Max Area (sq.ft)",
value=0,
step=100,
help="Max Area in square feet",
)
# Column 2: Floor Range
with col2:
floor_col1, floor_col2 = st.columns(2)
with floor_col1:
min_floor = st.text_input(
"๐Ÿ”ข Min Floor",
placeholder="e.g., G, 1, 2...",
help="Min Floor Number"
)
with floor_col2:
max_floor = st.text_input(
"๐Ÿ”ข Max Floor",
placeholder="e.g., 50, PH...",
help="Max Floor Number"
)
# Search execution
if st.button("๐Ÿ” Search", use_container_width=True):
filters = {
'query': query.strip() if query else None,
'min_price': min_price * 1_000_000 if min_price > 0 else None,
'max_price': max_price * 1_000_000 if max_price > 0 else None,
'min_area': min_area if min_area > 0 else None,
'max_area': max_area if max_area > 0 else None,
'min_floor': min_floor.strip() if min_floor else None,
'max_floor': max_floor.strip() if max_floor else None,
'developer': developer.strip() if developer else None,
'view': view.strip() if view else None,
'unit_type': unit_type.strip() if unit_type else None,
'n_results': n_results
}
# Execute search with all parameters
self.execute_search(filters)
def execute_search(self, filters):
"""Execute search with post-filtering and results display"""
try:
active_filters = {k: v for k, v in filters.items()
if v is not None and k != 'n_results'}
if not active_filters:
st.warning("Please enter at least one search criterion")
return
with st.spinner("Searching..."):
results = self.search_system.filter_search(**filters)
if not results:
st.info("No properties found matching your criteria")
return
# Create results DataFrame
df = pd.DataFrame([{
'Match': f"{r['similarity']:.1%}",
'Unit': r['metadata']['unit_code'],
'Type': r['metadata']['unit_type'].title(),
'Floor': r['metadata']['floor'],
'View': r['metadata']['view'].title(),
'Area': f"{float(str(r['metadata']['total_area']).replace(',', '')):,.0f}",
'Price': f"{float(str(r['metadata']['price']).replace(',', '')):,.2f}",
'Developer': r['metadata']['developer'].title()
} for r in results])
# Display results
# used_filters = [k for k, v in active_filters.items() if v is not None]
st.success(f"Found {len(results)} properties matching your criteria")
# Results tabs
tabs = st.tabs(["Results", "Analytics"])
with tabs[0]:
st.dataframe(
df,
use_container_width=True,
height=min(400, 35 + 35 * len(df))
)
with tabs[1]:
col1, col2 = st.columns(2)
with col1:
price_data = pd.to_numeric(
df['Price'].str.replace(',', ''),
errors='coerce'
)
fig_price = px.histogram(
price_data,
title="Price Distribution",
labels={'value': 'Price (AED)', 'count': 'Properties'},
color_discrete_sequence=['#4a90e2']
)
st.plotly_chart(fig_price, use_container_width=True)
with col2:
area_data = pd.to_numeric(
df['Area'].str.replace(',', ''),
errors='coerce'
)
fig_area = px.histogram(
area_data,
title="Area Distribution",
labels={'value': 'Area (sq.ft)', 'count': 'Properties'},
color_discrete_sequence=['#2ecc71']
)
st.plotly_chart(fig_area, use_container_width=True)
# Export option
st.download_button(
"๐Ÿ“ฅ Export Results",
df.to_csv(index=False),
f"property_results_{datetime.now().strftime('%Y%m%d_%H%M')}.csv",
"text/csv",
use_container_width=True
)
except Exception as e:
st.error(f"Search error: {str(e)}")
def render_file_management(self):
st.markdown("""
<div style='background: linear-gradient(120deg, #1a5f7a 0%, #66a6ff 100%);
padding: 15px;
border-radius: 10px;
margin-bottom: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);'>
<h3 style='color: white; margin: 0;'>๐Ÿ“‚ File Management</h3>
<p style='color: #e0e0e0; margin: 5px 0 0 0;'>upload and index your files</p>
</div>
""", unsafe_allow_html=True)
# Add this right after your file management header
with st.expander("๐Ÿ“‹ File Upload Guidelines", expanded=False):
st.markdown("""
##### ๐Ÿ“Œ Required Data Structure
Your CSV file must follow this structure:
""")
# Create sample DataFrame with realistic property data
sample_df = pd.DataFrame({
'UnitCode': ['UNIT001', 'UNIT002', 'UNIT003'],
'UnitType': ['4 Bedroom', '2 BR', 'Studio'],
'Floor': ['15', 'G', 'PH'],
'Developer': ['EMAAR', 'DAMAC', 'Select Group'],
'TotalArea': [2500, 1200, 550],
'AskingPrice': [3500000, 1800000, 950000],
'View': ['Sea View', 'Burj View', 'City View']
})
# Show sample data
st.dataframe(sample_df)
st.markdown("""
##### โš™๏ธ Column Specifications
- **UnitCode**: Unique identifier for each property
- **UnitType**: Property type (e.g., 4 Bedroom, 2 BR, Studio)
- **Floor**: Floor level (e.g., G, 1, 15, PH)
- **Developer**: Developer name
- **TotalArea**: Area in square feet (numeric)
- **AskingPrice**: Price in AED (numeric)
- **View**: View description
##### ๐Ÿ“ Notes:
- All columns are required
- Use consistent format for unit types
- Numeric values should not contain currency symbols
- Floor can be text (G = Ground, PH = Penthouse, etc.)
""")
# Download sample
st.download_button(
"๐Ÿ“ฅ Download Sample Template",
sample_df.to_csv(index=False),
"property_data_template.csv",
"text/csv",
help="Download a sample CSV template with correct structure"
)
# File Upload Section with Preview
col1, col2 = st.columns([2, 3])
with col1:
st.markdown("#### Upload New File")
uploaded_file = st.file_uploader("Choose CSV file", type=['csv'])
if uploaded_file:
try:
df = pd.read_csv(uploaded_file)
file_hash = get_file_hash(uploaded_file.getvalue())
# File validation
is_valid, missing_cols = validate_csv_structure(df)
if not is_valid:
st.error(f"Missing columns: {', '.join(missing_cols)}")
return
# Show data preview
st.markdown("##### Data Preview")
st.dataframe(
df.head(5),
use_container_width=True,
height=200
)
# File stats
st.markdown("##### File Statistics")
st.markdown("""
<style>
.small-font {
font-size: 14px !important; /* Adjust font size */
font-weight: normal !important; /* Use normal font weight */
}
</style>
""", unsafe_allow_html=True)
# Displaying metrics with custom HTML and smaller font size
stats_col1, stats_col2 = st.columns(2)
with stats_col1:
st.markdown(f"<div class='small-font'>Total Rows: {len(df)}</div>", unsafe_allow_html=True)
with stats_col2:
st.markdown(f"<div class='small-font'>File Size: {uploaded_file.size/1024:.1f} KB</div>", unsafe_allow_html=True)
# Index button with status
if st.button("๐Ÿ“ฅ Index File", use_container_width=True):
with st.spinner("Indexing..."):
file_path = save_uploaded_file(uploaded_file)
if file_path:
result = self.search_system.load_data(file_path, reset_collection=True)
if result["status"] == "success":
self.config.update_indexed_files(uploaded_file.name, file_hash)
st.success(f"โœ… Indexed {result['count']} properties")
st.balloons()
else:
st.error(f"โŒ Failed: {result['message']}")
except Exception as e:
st.error(f"Error reading file: {str(e)}")
with col2:
st.markdown("#### Indexed Files")
if self.config.config["indexed_files"]:
for filename, info in self.config.config["indexed_files"].items():
with st.container():
st.markdown(
f"""
<div style='background-color: white; padding: 15px; border-radius: 8px; margin: 10px 0; border-left: 4px solid #4a90e2;'>
<h5 style='margin: 0;'>{filename}</h5>
<p style='margin: 5px 0; color: #666;'>
Indexed: {datetime.fromisoformat(info['timestamp']).astimezone(DUBAI_TZ).strftime('%d %b, %H:%M')}
</p>
</div>
""",
unsafe_allow_html=True
)
col1, col2, col3 = st.columns([1,1,1])
with col1:
if st.button("๐Ÿ‘๏ธ Preview", key=f"preview_{filename}"):
try:
df = pd.read_csv(os.path.join(UPLOAD_FOLDER, filename))
st.dataframe(df.head())
except Exception as e:
st.error(f"Error previewing file: {str(e)}")
with col2:
if st.button("๐Ÿ“ฅ Export", key=f"export_{filename}"):
try:
df = pd.read_csv(os.path.join(UPLOAD_FOLDER, filename))
st.download_button(
"Download CSV",
df.to_csv(index=False),
filename,
"text/csv"
)
except Exception as e:
st.error(f"Error exporting file: {str(e)}")
with col3:
if st.button("๐Ÿ—‘๏ธ Remove", key=f"remove_{filename}"):
self.remove_file(filename)
st.rerun()
else:
st.info("No files indexed yet")
def remove_file(self, filename):
try:
# Remove from database
self.search_system.delete_properties(files=[filename])
# Remove from config
self.config.remove_indexed_file(filename)
# Remove physical file
file_path = os.path.join(UPLOAD_FOLDER, filename)
if os.path.exists(file_path):
os.remove(file_path)
st.success(f"โœ… Removed {filename}")
time.sleep(1) # Give time for success message
except Exception as e:
st.error(f"โŒ Error removing file: {str(e)}")
def render_analytics(self):
"""Render the analytics dashboard with enhanced styling and error handling."""
try:
st.markdown("""
<div style='background: linear-gradient(120deg, #1a5f7a 0%, #66a6ff 100%);
padding: 15px;
border-radius: 10px;
margin-bottom: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);'>
<h3 style='color: white; margin: 0;'>๐Ÿ“Š Analytics</h3>
<p style='color: #e0e0e0; margin: 5px 0 0 0;'>market analysis and insights</p>
</div>
""", unsafe_allow_html=True)
# reference
with st.expander("๐Ÿ“‹ Analytics Guidelines", expanded=False):
st.markdown("""
##### ๐Ÿ“Œ Analytics Overview
Our analytics dashboard offers five key analysis areas:
**1. ๐Ÿ“ˆ Price Analysis**
- Price distribution analysis
- Price per square foot trends
- Price segmentation across properties
- Distribution patterns and outliers
**2. ๐Ÿข Property Distribution**
- Property type breakdown
- Average price by property type
- Type-wise market share
- Detailed property metrics
**3. ๐Ÿ‘ฅ Developer Insights**
- Developer market share
- Average price by developer
- Developer performance metrics
- Portfolio size analysis
**4. ๐Ÿ“ Location Analysis**
- View distribution analysis
- Price premium by view
- Location-based pricing
- View preference patterns
**5. ๐Ÿ“Š Market Overview**
- Time-based price trends
- Property inventory trends
- Market momentum analysis
- Monthly price variations
##### ๐Ÿ’ก Interactive Features
Each visualization offers:
- Hover for detailed information
- Click on legends to filter
- Download charts as images
- Dynamic data filtering
""")
# Initialize analytics
analytics = RealEstateAnalytics(self.search_system)
# Get data with error handling
with st.spinner("Loading data..."):
df = analytics.get_all_properties()
if df.empty:
st.warning("No data available for analysis. Please ensure properties are indexed.")
return
# Summary Statistics Cards with consistent styling
col1, col2, col3, col4 = st.columns(4)
with col1:
total_value = df['AskingPrice'].sum()
st.metric(
"Portfolio Value",
f"{total_value/1e9:.2f}B AED",
f"{len(df):,} properties"
)
with col2:
avg_price = df['AskingPrice'].mean()
st.metric(
"Average Price",
f"{avg_price/1e6:.2f}M AED",
f"ยฑ{df['AskingPrice'].std()/1e6:.1f}M"
)
with col3:
avg_area = df['TotalArea'].mean()
st.metric(
"Average Area",
f"{avg_area:,.0f} sq.ft",
f"ยฑ{df['TotalArea'].std():,.0f}"
)
with col4:
st.metric(
"Total Units",
f"{len(df):,}",
f"{df['UnitType'].nunique()} types"
)
# Create analysis tabs
analysis_tabs = st.tabs([
"๐Ÿ“ˆ Price Analysis",
"๐Ÿข Property Distribution",
"๐Ÿ‘ฅ Developer Insights",
"๐Ÿ“ Location Analysis",
"๐Ÿ“Š Market Overview"
])
# Price Analysis Tab
with analysis_tabs[0]:
st.subheader("Price Distribution Analysis")
col1, col2 = st.columns(2)
with col1:
# Price histogram
fig_price = px.histogram(
df,
x='AskingPrice',
nbins=30,
title='Property Price Distribution',
labels={'AskingPrice': 'Price (AED)'},
color_discrete_sequence=['#4a90e2']
)
fig_price.update_layout(
showlegend=False,
plot_bgcolor='white',
paper_bgcolor='white'
)
st.plotly_chart(fig_price, use_container_width=True)
with col2:
# Price per sqft analysis
fig_psf = px.histogram(
df,
x='PricePerSqft',
nbins=30,
title='Price per Sqft Distribution',
labels={'PricePerSqft': 'Price per Sqft (AED)'},
color_discrete_sequence=['#2ecc71']
)
fig_psf.update_layout(
showlegend=False,
plot_bgcolor='white',
paper_bgcolor='white'
)
st.plotly_chart(fig_psf, use_container_width=True)
# Property Distribution Tab
with analysis_tabs[1]:
st.subheader("Property Type Analysis")
col1, col2 = st.columns(2)
with col1:
# Property type distribution
type_dist = df['UnitType'].value_counts()
fig_types = px.pie(
values=type_dist.values,
names=type_dist.index,
title='Property Type Distribution',
hole=0.4,
color_discrete_sequence=px.colors.sequential.Blues_r
)
st.plotly_chart(fig_types, use_container_width=True)
with col2:
# Average price by type
avg_price_type = df.groupby('UnitType')['AskingPrice'].mean().sort_values()
fig_price_type = px.bar(
x=avg_price_type.values,
y=avg_price_type.index,
orientation='h',
title='Average Price by Property Type',
labels={'x': 'Average Price (AED)', 'y': 'Property Type'},
color_discrete_sequence=['#4a90e2']
)
st.plotly_chart(fig_price_type, use_container_width=True)
# Developer Insights Tab
with analysis_tabs[2]:
st.subheader("Developer Analysis")
col1, col2 = st.columns(2)
with col1:
# Developer market share
dev_share = df.groupby('Developer')['UnitCode'].count().sort_values()
fig_dev = px.bar(
x=dev_share.values,
y=dev_share.index,
orientation='h',
title='Developer Market Share',
labels={'x': 'Number of Properties', 'y': 'Developer'},
color_discrete_sequence=['#4a90e2']
)
st.plotly_chart(fig_dev, use_container_width=True)
with col2:
# Average price by developer
avg_price_dev = df.groupby('Developer')['PricePerSqft'].mean().sort_values()
fig_dev_price = px.bar(
x=avg_price_dev.values,
y=avg_price_dev.index,
orientation='h',
title='Average Price/Sqft by Developer',
labels={'x': 'Average Price/Sqft (AED)', 'y': 'Developer'},
color_discrete_sequence=['#2ecc71']
)
st.plotly_chart(fig_dev_price, use_container_width=True)
# Location Analysis Tab
with analysis_tabs[3]:
st.subheader("View and Location Analysis")
col1, col2 = st.columns(2)
with col1:
# View distribution
view_dist = df['View'].value_counts()
fig_view = px.pie(
values=view_dist.values,
names=view_dist.index,
title='View Distribution',
hole=0.4,
color_discrete_sequence=px.colors.sequential.Blues_r
)
st.plotly_chart(fig_view, use_container_width=True)
with col2:
# Price premium by view
avg_price_view = df.groupby('View')['PricePerSqft'].mean().sort_values()
fig_view_price = px.bar(
x=avg_price_view.values,
y=avg_price_view.index,
orientation='h',
title='Average Price/Sqft by View',
labels={'x': 'Average Price/Sqft (AED)', 'y': 'View'},
color_discrete_sequence=['#4a90e2']
)
st.plotly_chart(fig_view_price, use_container_width=True)
# Market Overview Tab
with analysis_tabs[4]:
st.subheader("Market Overview")
# Time series analysis
df['IndexedDate'] = pd.to_datetime(df['IndexedDate'])
df['Month'] = df['IndexedDate'].dt.to_period('M')
monthly_trends = df.groupby('Month').agg({
'AskingPrice': 'mean',
'UnitCode': 'count'
}).reset_index()
monthly_trends['Month'] = monthly_trends['Month'].dt.to_timestamp()
# Price trends
fig_trends = px.line(
monthly_trends,
x='Month',
y='AskingPrice',
title='Average Property Price Trend',
labels={'AskingPrice': 'Average Price (AED)', 'Month': 'Month'},
color_discrete_sequence=['#4a90e2']
)
st.plotly_chart(fig_trends, use_container_width=True)
# Inventory trends
fig_inventory = px.line(
monthly_trends,
x='Month',
y='UnitCode',
title='Property Inventory Trend',
labels={'UnitCode': 'Number of Properties', 'Month': 'Month'},
color_discrete_sequence=['#2ecc71']
)
st.plotly_chart(fig_inventory, use_container_width=True)
except Exception as e:
st.error(f"Error in analytics dashboard: {str(e)}")
st.info("Please check your data and try refreshing the page.")
def run(self):
"""Main application entry point with authentication and navigation."""
st.set_page_config(
page_title="VARTUR ยฎ Real Estate Brokerage Search Engine",
page_icon="๐Ÿข",
layout="wide"
)
st.markdown(CUSTOM_CSS, unsafe_allow_html=True)
if 'authenticated' not in st.session_state:
st.session_state.authenticated = False
if not st.session_state.authenticated:
self.render_login()
return
# Top navigation bar
col1, col2 = st.columns([6,1])
with col1:
st.title("๐Ÿข Vartur Dashboard")
with col2:
if st.button("๐Ÿšช Logout", use_container_width=True):
st.session_state.authenticated = False
st.rerun()
self.render_dashboard()
# Main navigation tabs
tabs = st.tabs(["๐Ÿ” Search", "๐Ÿ“‚ File Management", "๐Ÿ“Š Analytics"])
with tabs[0]:
self.render_search()
with tabs[1]:
self.render_file_management()
with tabs[2]:
self.render_analytics()
if __name__ == "__main__":
app = EnhancedStreamlitApp()
app.run()