Introduction & Purpose

Welcome, developer! This is the definitive guide to the necta-fetcher Python package, expertly crafted by Someless Ado. This library is your key to unlocking programmatic access to student examination results from the NECTA (National Examinations Council of Tanzania) portal

necta-fetcher is designed to shield you from the intricate details of web scraping, such as managing HTTP sessions, handling CSRF (Cross-Site Request Forgery) tokens, and making direct API calls. This allows you to seamlessly integrate NECTA results into your applications, data analysis pipelines, or any other Python-based projects.

Critical Considerations & Disclaimer:

Chapter 1: Installation & Setup

Getting necta-fetcher up and running in your Python environment is a simple process facilitated by pip, Python's standard package installer.

1.1 Standard Installation (from PyPI)

Once necta-fetcher is officially published on the Python Package Index (PyPI), you can install it with a single command:

pip install necta-fetcher

It's good practice to ensure your pip installer is up-to-date: pip install --upgrade pip.

1.2 Installing from a Local Archive

If you have a local source archive file (e.g., necta_fetcher-0.1.0.tar.gz) or a pre-built wheel file (.whl):

# Example for a .tar.gz source distribution
pip install /path/to/your/necta_fetcher-0.1.0.tar.gz

# Example for a .whl (wheel) binary distribution
pip install /path/to/your/necta_fetcher-0.1.0-py3-none-any.whl

Remember to replace /path/to/your/ with the correct local file path.

1.3 For Developers: Installing from Source Code

If you're working with the package's source code (e.g., after cloning its Git repository):

# Navigate to the project's root directory (where setup.py is)
cd /path/to/necta_fetcher_project_root

# Standard install from source
pip install .

For active development on the necta-fetcher package itself, an "editable" install is highly beneficial. This creates a link to your source code, meaning any changes you make are immediately usable without reinstalling:

pip install -e .
Best Practice: Harnessing Virtual Environments

Isolating your project dependencies is crucial for stable and reproducible Python development. Virtual environments achieve this.

# Step 1: Create a new virtual environment
python -m venv my_app_env  # Or python3 -m venv my_app_env

# Step 2: Activate the environment
# On macOS/Linux:
source my_app_env/bin/activate
# On Windows (PowerShell):
my_app_env\Scripts\Activate.ps1
# On Windows (Command Prompt):
my_app_env\Scripts\activate.bat

# Step 3: Install necta-fetcher (and other dependencies) within this isolated environment
pip install necta-fetcher

Chapter 2: The NectaClient - Your Primary Interface

The central nervous system of the necta-fetcher package is the NectaClient class. All interactions with the NECTA portal are orchestrated through an instance of this class.

2.1 Client Initialization

To start, import the NectaClient and create an instance.

from necta_fetcher import NectaClient

# Create an instance of the client.
# For version 0.1.0, authentication details are managed internally by the package.
# Future versions may support explicit credential passing, e.g.:
# client = NectaClient(email="your_portal_email@example.com", password="your_portal_password")
client = NectaClient()

You can also customize the User-Agent HTTP header sent by the client. This can be useful for identifying your application if the NECTA portal administrators monitor traffic.

client = NectaClient(user_agent="MyUniversityResultsAggregator/2.1 (admin@myuniversity.edu)")

A Note on Authentication (v0.1.0): The current version (0.1.0) of NectaClient is configured with a specific set of built-in credentials for its operations. For production, enterprise, or any scenario requiring distinct user authentication, ensure you are utilizing a version of the library that explicitly permits secure, user-defined credential input. This documentation will be updated when such features are available.

Chapter 3: Retrieving Student Results with fetch_student_results()

The flagship method for acquiring examination results is client.fetch_student_results(). This powerful method encapsulates the entire workflow: ensuring the client is logged in (performing login if necessary), refreshing security tokens, making the API request, and processing the response.

3.1 Method Signature Deep Dive:

NectaClient.fetch_student_results(index_string: str, year: str, exam_level: str) -> dict

3.2 Parameter Breakdown:

Parameter Python Type Detailed Description Example Values
index_string str The student's complete and official examination index number. The package is generally case-insensitive to the prefix (S/P), but it's good practice to use uppercase. The format typically follows "S####-####" or "P####-####". "S1143-0115"
"P0101-0023"
year str The year the student sat for or completed the specified examination level. This must be provided as a string (e.g., "2022", not the integer 2022). "2022"
"2018"
exam_level str The precise code for the examination level. This value is case-sensitive and must correspond to the identifiers used by the NECTA portal's API. Commonly used levels include:
  • "CSEE": Certificate of Secondary Education Examination (Form IV)
  • "ACSEE": Advanced Certificate of Secondary Education Examination (Form VI)
  • "PSLE": Primary School Leaving Examination
If you are unsure or dealing with other examination types, you may need to inspect network requests on the NECTA portal or test with known values.
"CSEE"
"ACSEE"

3.3 Return Value:

Upon successful execution and finding the student's results, this method returns a Python dict (dictionary). This dictionary is a direct representation of the "data" portion of the JSON response from the NECTA API for the specific student.

3.4 Potential Exceptions:

This method can raise various exceptions derived from NectaError if any part of the process fails. Comprehensive error handling is crucial (see Chapter 4).

3.5 In-Depth Usage Example & Output Exploration:

from necta_fetcher import NectaClient, NectaError, NectaStudentNotFoundError
import pprint # For visually appealing dictionary printing

# 1. Initialize the NectaClient
client = NectaClient()

# 2. Define the student query parameters
student_index = "S1143-0115"  # A known working example from previous tests
examination_year = "2022"
examination_level = "CSEE"

print(f"šŸš€ Initiating NECTA results fetch for:")
print(f"   Index Number: {student_index}")
print(f"   Year:         {examination_year}")
print(f"   Level:        {examination_level}")
print("-" * 50)

try:
    # 3. Call the fetch_student_results method
    # This single call handles login, token management, and the API request.
    results_dictionary = client.fetch_student_results(
        index_string=student_index,
        year=examination_year,
        exam_level=examination_level
    )

    # 4. Process the successful response
    print("\nšŸŽ‰ Results Retrieved Successfully!\n")
    
    print("Raw Python Dictionary Output:")
    pprint.pprint(results_dictionary, indent=2, width=100) # Pretty print

    # 5. Accessing and displaying specific data fields
    print("\n--- Detailed Breakdown ---")
    full_name = f"{results_dictionary.get('first_name', 'N/A')} {results_dictionary.get('middle_name', '')} {results_dictionary.get('last_name', 'N/A')}".strip()
    print(f"Student Name:   {full_name}")
    print(f"Index Number:   {results_dictionary.get('index_number', 'N/A')}")
    print(f"Overall Division: {results_dictionary.get('division', 'N/A')}")
    print(f"Total Points:   {results_dictionary.get('points', 'N/A')}")
    
    # Accessing subjects (which is a list of dictionaries)
    subjects_list = results_dictionary.get('subjects', [])
    if subjects_list:
        print("\nšŸ“š Subject Performance:")
        for subject_details in subjects_list:
            subject_name = subject_details.get('subject_name', 'Unknown Subject')
            grade = subject_details.get('grade', 'N/A')
            print(f"  - {subject_name:<25}: {grade}") # Formatted output
    else:
        print("\nā„¹ļø No detailed subject information was found in the results.")

except NectaStudentNotFoundError:
    print(f"\nāš ļø Error: The student with index '{student_index}' for year {examination_year}, level {examination_level} was NOT FOUND on the NECTA portal.")
    print("   Please verify the accuracy of the entered details or try alternative parameters.")
except NectaLoginError as e:
    print(f"\nšŸ”„ Login Error: Authentication with the NECTA portal failed. Details: {e}")
except NectaTokenError as e:
    print(f"\nšŸ”‘ Token Error: An issue occurred with security (CSRF) token management. Details: {e}")
except NectaResultError as e:
    print(f"\nšŸ“‹ Result Error: A problem occurred while fetching or interpreting the results data. Details: {e}")
except NectaRequestError as e:
    print(f"\n🌐 Request Error: A network connectivity or HTTP request issue was encountered. Details: {e}")
except NectaError as e: # Catch-all for other necta_fetcher specific errors
    print(f"\nāš™ļø Necta-Fetcher Library Error: {e}")
except Exception as e: # Catch any other unexpected Python errors
    print(f"\nšŸ’„ An Unforeseen System Error Occurred: {e}")
    import traceback
    traceback.print_exc() # Provides full traceback for debugging
finally:
    print("-" * 50)
    print("Fetch attempt concluded.")

3.6 Example Successful JSON Output Structure (Illustrative)

The results_dictionary you receive from a successful call to fetch_student_results() will typically conform to a structure like the following (based on observed API responses). The exact fields and their presence can vary.

{
  "index_number": "S1143-0115",        // The student's unique index
  "first_name": "SAMWEL",             // Student's first name
  "middle_name": "N",                 // Student's middle name (can be empty or N/A)
  "last_name": "ADONIA",              // Student's last name
  "sex": "MALE",                      // Gender (may not always be present)
  "school_name": "UBUNGO ISLAMIC HIGH SCHOOL", // Name of the examination center/school
  "division": "III",                  // Overall division achieved
  "points": "24",                     // Total points scored
  "subjects": [                       // A list of subjects taken
    {
      "subject_name": "BASIC MATHEMATICS",
      "grade": "D"
    },
    {
      "subject_name": "BIOLOGY",
      "grade": "D"
    },
    {
      "subject_name": "CHEMISTRY",
      "grade": "C"
    },
    {
      "subject_name": "PHYSICS",
      "grade": "C"
    },
    {
      "subject_name": "ENGLISH LANGUAGE",
      "grade": "C"
    },
    {
      "subject_name": "KISWAHILI",
      "grade": "C"
    },
    {
      "subject_name": "PHYSICAL EDUCATION",
      "grade": "D"
    },
    {
      "subject_name": "GEOGRAPHY",
      "grade": "D"
    },
    {
      "subject_name": "HISTORY",
      "grade": "D"
    },
    {
      "subject_name": "CIVICS",
      "grade": "F"
    }
    // ... other subjects may be listed
  ],
  "center_number": "S1143",           // Code for the examination center
  "exam_type": "CSEE",                // Type of examination (e.g., CSEE, ACSEE)
  "exam_year": "2022"                 // Year of examination
  // Other fields like 'status', 'remarks' might also be present.
}
Understanding the API Response: The necta-fetcher package endeavors to return the primary "data" payload from the NECTA API's JSON response with minimal modification. If the NECTA API alters its response schema (e.g., adds new fields, renames existing ones, or changes data types), the dictionary returned by this package will directly reflect those modifications. It is always advisable to inspect the structure of the returned dictionary, especially if you encounter unexpected behavior or missing data after a portal update.

Chapter 4: Mastering Error Handling

Effective error handling is paramount for building resilient applications. The necta-fetcher package equips you with a clear hierarchy of custom exceptions, all descending from the base NectaError. This allows for granular control over how your application responds to various failure scenarios.

Deep Dive into the Exception Hierarchy:

  • NectaError (Base Exception for the package)

    Catch this if you want a general handler for any issue originating from necta-fetcher.

    • NectaLoginError:

      Signifies a failure during the authentication process. This can occur due to:

      • Incorrect credentials (if the package version allows user-input credentials).
      • Changes in the NECTA portal's login page HTML structure that prevent the library from finding the CSRF token or form fields.
      • Network connectivity issues specifically encountered during the login attempt.
      • The portal explicitly rejecting the login attempt (e.g., account locked, invalid credentials).

    • NectaTokenError:

      Indicates a problem related to CSRF (Cross-Site Request Forgery) tokens. These tokens are essential for security. Errors can arise if:

      • The library cannot locate CSRF tokens on the login page or subsequent authenticated pages.
      • Extracted tokens are malformed or stale.
      • The server rejects a request due to an invalid or missing CSRF token.

    • NectaRequestError:

      This exception covers broader network communication problems or unexpected HTTP error responses from the NECTA portal during any non-login/non-result-specific API interaction. Examples include:

      • DNS resolution failures.
      • Connection timeouts or refused connections.
      • Receiving HTTP status codes like 500 (Internal Server Error), 403 (Forbidden), 401 (Unauthorized, if not handled by LoginError) from the portal.
      • SSL/TLS handshake failures.

    • NectaResultError (Base for issues specific to fetching/processing results)

      This is raised for problems occurring after a successful login and token acquisition, specifically during the attempt to fetch or interpret student results.

      • NectaStudentNotFoundError:

        A very common and important exception. It is raised specifically when:

        • The NECTA API explicitly indicates that no results were found for the given student criteria (index, year, level). This might be via a "success": false response with a "not found" message.
        • The API returns a placeholder "N/A" type of response when queried for a specific student, implying that detailed records for that particular student are absent, even if the school/center itself exists.

      • (Other general NectaResultError instances): These can occur if:
        • The API returns a generic error (e.g., "success": false with a non-"not found" error message).
        • The response from the results endpoint is not valid JSON and cannot be decoded.
        • The JSON response is successfully decoded but does not conform to the expected structure (e.g., missing the "data" field, or the "data" field is not a dictionary when a specific student's results are anticipated).

By structuring your try...except blocks from most specific to most general (e.g., catching NectaStudentNotFoundError before NectaResultError, and NectaResultError before NectaError), you can tailor your application's response to different failure modes.

# Recommended robust error handling pattern
client = NectaClient()
try:
    student_data = client.fetch_student_results(
        index_string="SXXXX-YYYY", 
        year="YYYY", 
        exam_level="LEVEL"
    )
    # ... process student_data ...
    print("Results processed successfully.")

except NectaStudentNotFoundError as e_student_not_found:
    print(f"Notification: Results for the specified student were not found. Details: {e_student_not_found}")
    # Action: Inform the user, log the query, suggest checking inputs.

except NectaLoginError as e_login:
    print(f"Critical Error: Login to NECTA portal failed. Details: {e_login}")
    # Action: Alert administrator, check system's network, halt operations requiring login.

except NectaTokenError as e_token:
    print(f"Warning: CSRF token issue encountered. The portal's security mechanism might have changed. Details: {e_token}")
    # Action: May require a library update or manual investigation of portal changes.

except NectaResultError as e_result: # Catches other result-related issues not covered by StudentNotFound
    print(f"Error: There was an issue obtaining or interpreting the results data from NECTA. Details: {e_result}")
    # Action: Log the error and the problematic query, potentially retry later.

except NectaRequestError as e_request:
    print(f"Network/Server Error: Communication with the NECTA portal failed. Details: {e_request}")
    # Action: Check internet connectivity, inform user to try again later.

except NectaError as e_necta_base: # Fallback for any other necta-fetcher specific error
    print(f"Library Error: A general error occurred within the necta-fetcher package. Details: {e_necta_base}")

except requests.exceptions.RequestException as e_requests_lib: # Catch underlying requests library errors if not wrapped
    print(f"Low-Level Network Error: An error occurred in the underlying HTTP requests library. Details: {e_requests_lib}")
    # This is more for debugging library issues.

except Exception as e_unexpected: # General Python errors
    print(f"System Error: An unexpected error occurred in the application. Details: {e_unexpected}")
    # Action: Log detailed traceback for developer review.
    # import traceback
    # traceback.print_exc()

Chapter 5: Package Internals & Versioning

5.1 Retrieving Package Version

You can easily ascertain the installed version of the necta-fetcher package within your Python code. This is useful for logging, debugging, or conditional logic based on package capabilities.

import necta_fetcher

current_version = necta_fetcher.__version__
print(f"You are currently using necta-fetcher version: {current_version}")

5.2 A Glimpse into the Workflow

When you invoke client.fetch_student_results(), the NectaClient executes a sequence of operations:

  1. Authentication Check & Login: If not already authenticated in the current session, it initiates the login procedure. This involves:
    1. Sending a GET request to the NECTA portal's login page.
    2. Parsing the HTML of the login page to extract a necessary CSRF token (often named _token).
    3. Submitting a POST request to the login URL. This request includes the required credentials (currently internally managed by the package) and the extracted CSRF token.
    4. Validating successful login, typically by inspecting the URL of the page redirected to or specific content elements on that page.
  2. Action-Specific Token Refresh: Following a successful login, the client visits another authenticated page on the portal (e.g., the main dashboard or homepage). The purpose is to parse this page and secure a CSRF token that is specifically designated for subsequent actions, such as fetching results. This "action token" can differ from the login CSRF token and is often embedded within HTML forms (like a logout form) or specified in tags.
  3. Results API Request Formulation & Execution:
    1. A JSON payload is meticulously constructed. This payload includes the year, number (which is the full student index string), level, and the freshly acquired action CSRF token.
    2. Appropriate HTTP headers are assembled (e.g., Content-Type: application/x-www-form-urlencoded, X-Requested-With: XMLHttpRequest, and potentially an X-XSRF-TOKEN header if the corresponding cookie is present).
    3. A POST request is dispatched to the designated NECTA results API endpoint (e.g., a URL ending in .../candidates/nectaResult).
  4. API Response Interpretation & Validation:
    1. The HTTP status code of the API's response is scrutinized. Any non-2xx status typically indicates an error.
    2. If the request is successful (e.g., status 200 OK), the response body is decoded from JSON format.
    3. The decoded JSON is subjected to basic structural validation (e.g., verifying the presence of a "success": true flag and a "data" field).
    4. A crucial check is performed to ensure the data within the "data" field corresponds to the queried student (by matching index numbers) and is not merely a placeholder (e.g., by checking if first_name is "N/A" and subjects list is empty).
    5. The validated student data dictionary is returned to the caller, or an appropriate exception is raised if any step fails.

The NectaClient leverages an internal requests.Session() object. This powerful feature from the requests library automatically manages cookies (such as session identifiers and XSRF tokens) across the multiple HTTP requests involved in the workflow, maintaining the authenticated state with the NECTA portal.

Chapter 6: Complete Runnable Terminal Example

Here is a self-contained Python script that you can copy, paste, and run directly in your terminal (after installing the necta-fetcher package). This example demonstrates a common use case: prompting the user for student details and displaying the fetched results.

#!/usr/bin/env python3
# Filename: necta_terminal_app.py

import re
import pprint
from necta_fetcher import (
    NectaClient,
    __version__ as necta_fetcher_version,
    NectaError,
    NectaStudentNotFoundError,
    NectaLoginError,
    NectaTokenError,
    NectaResultError,
    NectaRequestError
)

def get_user_input(prompt_message: str, validation_regex: str = None, error_message: str = "Invalid input.") -> str:
    """Generic function to get and validate user input."""
    while True:
        user_val = input(f"{prompt_message}: ").strip()
        if not validation_regex or re.match(validation_regex, user_val, re.IGNORECASE):
            return user_val
        print(f"āŒ Error: {error_message}")

def main_terminal_application():
    """Runs the interactive terminal application to fetch NECTA results."""
    print("=" * 70)
    print(f"    NECTA Results Interactive Terminal Client")
    print(f"    (Powered by necta-fetcher v{necta_fetcher_version} by Someless Ado)")
    print("=" * 70)
    print("This tool uses the 'necta-fetcher' package to retrieve student results.")
    print("Ensure the package is installed (`pip install necta-fetcher`).")
    print("Note: The current package version uses an internally managed login.\n")

    # Get validated inputs
    full_index = get_user_input(
        prompt_message="Enter Student's Full Index Number (e.g., S1234-0001)",
        validation_regex=r"^[SP]\d{4}-\d{4}$",
        error_message="Format must be Sxxxx-yyyy or Pxxxx-yyyy (e.g., S1234-0001)."
    ).upper()

    year = get_user_input(
        prompt_message="Enter Examination Year (e.g., 2022)",
        validation_regex=r"^(20[0-2]\d|199\d)$", # Approx 1990-2029
        error_message="Please enter a 4-digit year (e.g., 2022)."
    )

    exam_level = get_user_input(
        prompt_message="Enter Examination Level (CSEE, ACSEE, PSLE)",
        validation_regex=r"^(CSEE|ACSEE|PSLE)$",
        error_message="Accepted levels are CSEE, ACSEE, or PSLE."
    ).upper()

    print("\n" + "~" * 30)
    print(f"ā³ Preparing to fetch results for:")
    print(f"  Index: {full_index}")
    print(f"  Year:  {year}")
    print(f"  Level: {exam_level}")
    print("~" * 30 + "\n")

    # Initialize the NectaClient from the installed package
    client = NectaClient()

    try:
        # Attempt to fetch the results
        student_results_data = client.fetch_student_results(
            index_string=full_index,
            year=year,
            exam_level=exam_level
        )
        
        print("\n" + "šŸŽ‰" * 15 + " RESULTS OBTAINED " + "šŸŽ‰" * 15)
        print("Student Details:")
        pprint.pprint(student_results_data, indent=2, width=100)

    except NectaStudentNotFoundError as e:
        print(f"\n🚫 STUDENT NOT FOUND: {e}")
        print("   Please ensure all details (index, year, level) are correct and try again.")
    except NectaLoginError as e:
        print(f"\nšŸ”„ LOGIN FAILURE: {e}")
        print("   Could not authenticate with the NECTA portal. Check network or system configuration.")
    except NectaTokenError as e:
        print(f"\nšŸ”‘ TOKEN ACQUISITION FAILURE: {e}")
        print("   There was an issue with security tokens. The portal might have updated its systems.")
    except NectaResultError as e:
        print(f"\nšŸ“‹ RESULTS PROCESSING ERROR: {e}")
        print("   An error occurred while trying to fetch or understand the results data.")
    except NectaRequestError as e:
        print(f"\n🌐 NETWORK/REQUEST ERROR: {e}")
        print("   A problem occurred during communication with the NECTA portal. Check your internet connection.")
    except NectaError as e:
        print(f"\nāš™ļø NECTA-FETCHER LIBRARY ERROR: {e}")
    except Exception as e:
        print(f"\nšŸ’„ UNEXPECTED SYSTEM ERROR: {e}")
        print("   An unhandled error occurred. Please report this with the details below:")
        import traceback
        traceback.print_exc()
    finally:
        print("\n" + "=" * 70)
        print("Query attempt finished. Run the script again to fetch another result.")
        print("=" * 70)

if __name__ == "__main__":
    main_terminal_application()

Chapter 7: Future Directions & Community

The journey of necta-fetcher is ongoing. Someless Ado and potential future contributors envision several enhancements:

Feedback, bug reports, feature suggestions, and contributions from the developer community are highly valued. Please refer to the project's official repository (if one is made public) for contribution guidelines or to contact the developer, Someless Ado.

Staying Updated:

The digital landscape, especially web services and APIs, is constantly evolving. To ensure you have the latest features, bug fixes, and adaptations to any NECTA portal changes, periodically update your installed necta-fetcher package:

pip install --upgrade necta-fetcher