diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..016a1e9
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,2 @@
+# Default review owner
+* @xxg1413
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..a60cc3f
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+patreon: pxiaoer
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.github/ISSUE_TEMPLATE/broken-link.yml b/.github/ISSUE_TEMPLATE/broken-link.yml
new file mode 100644
index 0000000..2ca94a8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/broken-link.yml
@@ -0,0 +1,66 @@
+name: Broken or moved link
+description: Report a catalog URL that is permanently unavailable or has moved.
+title: "[Broken link]: "
+labels:
+ - broken-link
+body:
+ - type: markdown
+ attributes:
+ value: |
+ A single 403, 429, timeout, or transient 5xx does not prove a resource is broken. Include repeatable evidence so maintainers can distinguish blocking and rate limiting from removal.
+ - type: input
+ id: page
+ attributes:
+ label: FlyPython page or catalog ID
+ placeholder: ai-agents / openai-agents-sdk
+ validations:
+ required: true
+ - type: input
+ id: url
+ attributes:
+ label: Affected URL
+ placeholder: https://example.org/old-page
+ validations:
+ required: true
+ - type: input
+ id: observed-on
+ attributes:
+ label: Observation date
+ description: Use YYYY-MM-DD.
+ placeholder: "2026-08-31"
+ validations:
+ required: true
+ - type: dropdown
+ id: result
+ attributes:
+ label: Observed result
+ options:
+ - 404 Not Found
+ - 410 Gone
+ - Redirects to unrelated content
+ - DNS or TLS failure
+ - 403 Forbidden
+ - 429 Too Many Requests
+ - Timeout or 5xx
+ - Other
+ validations:
+ required: true
+ - type: textarea
+ id: evidence
+ attributes:
+ label: Reproduction and evidence
+ description: Include what you tried, whether it reproduced, and any final redirect URL. Do not include cookies, tokens, or personal data.
+ validations:
+ required: true
+ - type: input
+ id: replacement
+ attributes:
+ label: Official replacement URL
+ description: Optional. Provide only a canonical replacement from the same publisher or project.
+ - type: checkboxes
+ id: confirmation
+ attributes:
+ label: Confirmation
+ options:
+ - label: I checked that this report does not expose a credential or security vulnerability.
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..297a377
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Python learning questions and discussion
+ url: https://github.com/flypythoncom/python/discussions
+ about: Ask for learning advice, compare resources, or start a general discussion.
+ - name: Private security report
+ url: https://github.com/flypythoncom/python/security/advisories/new
+ about: Report vulnerabilities privately. Do not disclose them in a public issue.
diff --git a/.github/ISSUE_TEMPLATE/resource-proposal.yml b/.github/ISSUE_TEMPLATE/resource-proposal.yml
new file mode 100644
index 0000000..4e16d2e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/resource-proposal.yml
@@ -0,0 +1,99 @@
+name: Resource proposal
+description: Propose an official Python or AI-agent resource for the reviewed catalog.
+title: "[Resource]: "
+labels:
+ - resource-proposal
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for helping improve FlyPython. Read the [curation policy](https://github.com/flypythoncom/python/blob/master/docs/CURATION_POLICY.md) before submitting. General learning questions belong in Discussions.
+ - type: input
+ id: title
+ attributes:
+ label: Resource name
+ placeholder: Python Packaging User Guide
+ validations:
+ required: true
+ - type: input
+ id: url
+ attributes:
+ label: Canonical URL
+ description: Use the official publisher, standard, documentation, or project URL.
+ placeholder: https://packaging.python.org/
+ validations:
+ required: true
+ - type: dropdown
+ id: path
+ attributes:
+ label: Learning path
+ options:
+ - foundations
+ - web-apis
+ - automation
+ - ai-agents
+ validations:
+ required: true
+ - type: dropdown
+ id: source-type
+ attributes:
+ label: Source type
+ options:
+ - official-docs
+ - official-standard
+ - official-project
+ validations:
+ required: true
+ - type: dropdown
+ id: level
+ attributes:
+ label: Learner level
+ options:
+ - beginner
+ - intermediate
+ - advanced
+ - all-levels
+ validations:
+ required: true
+ - type: dropdown
+ id: language
+ attributes:
+ label: Resource language
+ description: Select the catalog's machine-readable language value.
+ options:
+ - en
+ - zh
+ - multilingual
+ validations:
+ required: true
+ - type: textarea
+ id: rationale
+ attributes:
+ label: Why it belongs
+ description: Explain the reader outcome, why this source is authoritative, and where it fits. Use your own words.
+ placeholder: This official guide helps intermediate readers...
+ validations:
+ required: true
+ - type: textarea
+ id: evidence
+ attributes:
+ label: Maintenance, licensing, and safety evidence
+ description: Include the review date, ownership or license, current maintenance evidence, API-key or paid-access requirements, and material risks.
+ validations:
+ required: true
+ - type: checkboxes
+ id: confirmations
+ attributes:
+ label: Submission checks
+ options:
+ - label: I read the curation policy and verified the canonical URL.
+ required: true
+ - label: The rationale and classification were reviewed by a person and were not generated by an LLM or web-search API.
+ required: true
+ - label: I disclosed any affiliation, sponsorship, or self-promotion in the additional context below.
+ required: true
+ - type: textarea
+ id: context
+ attributes:
+ label: Additional context or affiliation
+ description: State whether you maintain, work for, sponsor, or receive compensation from the resource.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..b82311f
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,43 @@
+## Summary
+
+Describe the problem, the change, and the reader or maintainer outcome.
+
+## Change type
+
+- [ ] Catalog resource or classification
+- [ ] Bilingual editorial content
+- [ ] Site or build code
+- [ ] Tests, tooling, or workflow
+- [ ] Governance or documentation
+
+## Evidence and scope
+
+- Related issue:
+- Source or maintenance evidence:
+- User-visible pages affected:
+- Intentionally out of scope:
+
+For catalog changes, explain the official source, review date, license or
+ownership, maintenance state, API-key or payment requirements, and safety risk.
+Disclose any affiliation or self-promotion.
+
+## Validation
+
+List the actual results, not only the commands you intended to run.
+
+- [ ] `python -m pytest`
+- [ ] `python tools/validate_catalog.py`
+- [ ] `bundle exec jekyll build`
+- [ ] `bundle exec ruby tests/render_security_test.rb`
+- [ ] English and Chinese output remain consistent
+- [ ] No secrets, generated reports, local build output, or analytics IDs were committed
+
+Catalog links are fetched only by the scheduled or manually dispatched
+workflow, not by pull-request CI.
+
+## Permissions and review
+
+- [ ] I have the right to submit this contribution.
+- [ ] I retained third-party notices and did not relicense third-party material.
+- [ ] Human reviewers wrote or verified all resource descriptions and classifications.
+- [ ] I read `CONTRIBUTING.md` and `docs/CURATION_POLICY.md`.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..94a2b78
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,41 @@
+version: 2
+updates:
+ - package-ecosystem: bundler
+ directory: "/"
+ schedule:
+ interval: weekly
+ day: monday
+ time: "09:00"
+ timezone: Asia/Shanghai
+ open-pull-requests-limit: 5
+ groups:
+ jekyll-dependencies:
+ patterns:
+ - "github-pages"
+ - "jekyll-*"
+
+ - package-ecosystem: pip
+ directory: "/"
+ schedule:
+ interval: weekly
+ day: monday
+ time: "09:15"
+ timezone: Asia/Shanghai
+ open-pull-requests-limit: 5
+ groups:
+ python-dependencies:
+ patterns:
+ - "*"
+
+ - package-ecosystem: github-actions
+ directory: "/"
+ schedule:
+ interval: weekly
+ day: monday
+ time: "09:30"
+ timezone: Asia/Shanghai
+ open-pull-requests-limit: 5
+ groups:
+ actions:
+ patterns:
+ - "*"
diff --git a/.github/workflows/link-audit.yml b/.github/workflows/link-audit.yml
new file mode 100644
index 0000000..8fc6814
--- /dev/null
+++ b/.github/workflows/link-audit.yml
@@ -0,0 +1,45 @@
+name: Catalog link audit
+
+on:
+ schedule:
+ - cron: "23 1 * * 1"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: catalog-link-audit
+ cancel-in-progress: false
+
+jobs:
+ audit:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ - name: Set up Python
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ with:
+ python-version-file: ".python-version"
+ cache: pip
+
+ - name: Install Python dependencies
+ run: python -m pip install --disable-pip-version-check -r requirements.lock.txt
+
+ - name: Prepare report directory
+ run: mkdir -p reports
+
+ - name: Check every catalog link
+ run: python tools/check_links.py --mode all --output reports/link-check.json
+
+ - name: Upload link report
+ if: ${{ always() }}
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: catalog-link-report-${{ github.run_id }}
+ path: reports/link-check.json
+ if-no-files-found: error
+ retention-days: 14
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
new file mode 100644
index 0000000..2fc4042
--- /dev/null
+++ b/.github/workflows/validate.yml
@@ -0,0 +1,48 @@
+name: Validate
+
+on:
+ pull_request:
+ push:
+ branches:
+ - master
+
+permissions:
+ contents: read
+
+concurrency:
+ group: validate-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ - name: Set up Python
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
+ with:
+ python-version-file: ".python-version"
+ cache: pip
+
+ - name: Install Python dependencies
+ run: python -m pip install --disable-pip-version-check -r requirements-dev.lock.txt
+
+ - name: Run tests
+ run: python -m pytest
+
+ - name: Validate catalog
+ run: python tools/validate_catalog.py
+
+ - name: Set up Ruby and Bundler
+ uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
+ with:
+ bundler-cache: true
+
+ - name: Build Jekyll site
+ run: bundle exec jekyll build
+
+ - name: Verify catalog output escaping
+ run: bundle exec ruby tests/render_security_test.rb
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..90520c2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,54 @@
+# Jekyll
+_site/
+.sass-cache/
+.jekyll-cache/
+.jekyll-metadata
+
+# Ruby
+.bundle/
+vendor/
+
+# OS
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# Editor
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# Logs
+*.log
+
+# Runtime data
+pids
+*.pid
+*.seed
+
+# Environment variables
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# Temporary files
+tmp/
+temp/
+.tmp/
+
+# Python
+.venv/
+venv/
+__pycache__/
+*.pyc
+
+# Generated reports
+reports/
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..28d9a01
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.12.13
diff --git a/.ruby-version b/.ruby-version
new file mode 100644
index 0000000..a0891f5
--- /dev/null
+++ b/.ruby-version
@@ -0,0 +1 @@
+3.3.4
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..3aef345
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,38 @@
+# FlyPython repository guide
+
+## Mission
+
+This repository is the broad, community-maintained resource catalog behind
+FlyPython. Keep `flypython.com` focused on editorial learning paths and a small
+featured set; keep this repository focused on the larger reviewed catalog and
+its contribution workflow.
+
+## Content rules
+
+- Prefer official documentation and primary sources.
+- Every catalog entry must include a stable ID, path, source type, level,
+ rationale, review date, status, and any key or safety requirements.
+- Do not call a project production-ready without current maintenance, licensing,
+ security, and adoption evidence.
+- Do not use an LLM or web-search API to generate resource descriptions.
+ Descriptions and classifications require human review.
+- Keep English and Chinese pages generated from the same canonical resource
+ data. Do not hand-copy the full catalog into multiple Markdown files.
+- Treat HTTP 403, 429, and transient 5xx responses as review-needed states, not
+ automatic proof that a resource is broken.
+
+## Change workflow
+
+- Work on a feature branch and preserve unrelated contributor changes.
+- Run the repository validation workflow before committing.
+- Keep external-link checks read-only, rate-limited, retryable, and blocked from
+ private or loopback network targets.
+- Do not add API keys, tokens, analytics IDs, or generated reports to git.
+- After merge, verify the GitHub Pages deployment and the production domain.
+
+## Review priorities
+
+1. Content accuracy and source quality.
+2. Deterministic generation and bilingual parity.
+3. Build, schema, link, accessibility, and SEO checks.
+4. Clear contribution and licensing boundaries.
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..a58b41e
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,39 @@
+# Changelog
+
+This file records notable repository and catalog-process changes. The Git
+history remains the source for changes made before this changelog was added.
+
+## [Unreleased]
+
+### Added
+
+- A single bilingual catalog source with 21 reviewed primary resources across
+ Python foundations, web and APIs, automation, and AI-agent paths.
+- Catalog schema validation and a safe external-link auditor with tests,
+ retry/backoff behavior, JSON reports, and SSRF/DNS-rebinding protection.
+- English and Chinese catalog pages generated from the same resource data,
+ plus a 1200x630 social preview image.
+- Contribution, conduct, security, and resource-curation policies.
+- Structured issue forms and a pull request review checklist.
+- Dependency update configuration and validation workflows.
+- Scheduled, manually dispatchable catalog-link auditing that does not run on
+ untrusted pull requests.
+
+### Changed
+
+- Reframed the repository as the broad, community-maintained catalog behind
+ FlyPython while keeping the README files as short entry points.
+- Pinned the Python 3.12 and Ruby 3.3 build toolchains and their validation
+ dependencies for reproducible local and CI checks.
+
+### Fixed
+
+- Corrected repository links, dynamic resource counts, bilingual metadata,
+ canonical and hreflang output, robots rules, and duplicate heading/meta tags.
+- Escaped catalog-controlled HTML output, audited same-origin catalog links,
+ and made review-needed network results fail the scheduled audit.
+
+### Removed
+
+- Removed the legacy `README_cn` site route without adding a redirect, along
+ with unused feed, navigation, collection, theme, and inline-style config.
diff --git a/CNAME b/CNAME
new file mode 100644
index 0000000..bfb6cb6
--- /dev/null
+++ b/CNAME
@@ -0,0 +1 @@
+python.flypython.com
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..3e295ed
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,124 @@
+# Contributor Covenant Code of Conduct
+
+## Our pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic
+status, nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our standards
+
+Examples of behavior that contributes to a positive environment include:
+
+- Demonstrating empathy and kindness toward other people
+- Being respectful of differing opinions, viewpoints, and experiences
+- Giving and gracefully accepting constructive feedback
+- Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+- Focusing on what is best not just for us as individuals, but for the overall
+ community
+
+Examples of unacceptable behavior include:
+
+- The use of sexualized language or imagery, and sexual attention or advances
+ of any kind
+- Trolling, insulting or derogatory comments, and personal or political attacks
+- Public or private harassment
+- Publishing others' private information, such as a physical or email address,
+ without their explicit permission
+- Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards
+of acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official email address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+hello@flypython.com. All complaints will be reviewed and investigated promptly
+and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary ban
+
+**Community impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent ban
+
+**Community impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][mozilla-coc].
+
+[homepage]: https://www.contributor-covenant.org
+[mozilla-coc]: https://github.com/mozilla/diversity
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..0883f29
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,102 @@
+# Contributing to FlyPython
+
+FlyPython is a bilingual, reviewed catalog of durable Python and AI-agent
+resources. Contributions should improve accuracy, source quality, or the
+catalog workflow. General Python questions belong in
+[GitHub Discussions](https://github.com/flypythoncom/python/discussions).
+
+Before contributing, read the [curation policy](docs/CURATION_POLICY.md).
+
+## Permissions
+
+This repository currently does not grant a general license to reuse its
+content or code. Public visibility is not permission to copy, redistribute,
+or relicense repository material. Contributors must submit only material
+they have the right to submit and must preserve any required third-party
+notices. A contribution does not change the terms of third-party material.
+
+## Propose a change
+
+Use the matching issue form before a larger change:
+
+- **Resource proposal** for a new catalog entry.
+- **Broken link** for an unreachable or replaced resource.
+- **Security report** for a vulnerability. Follow [SECURITY.md](SECURITY.md)
+ instead of opening a public issue.
+
+Small typo, metadata, or maintenance fixes may go directly to a pull request.
+Keep each pull request focused on one purpose.
+
+## Catalog changes
+
+`_data/resources.yml` is the canonical catalog source. Do not add or change a
+resource only in a rendered Markdown page.
+
+Each resource entry must include these fields:
+
+- `id`, `path`, `title`, and `url`
+- `source_type`, `level`, and `language`
+- `why_en` and `why_zh`
+- `reviewed_on` and `status`
+- `requires_key`, `risk`, and `featured`
+
+Descriptions and classifications require human review. Do not use an LLM or a
+web-search API to generate them. Prefer official documentation, official
+standards, and official project pages.
+
+A 403, 429, or transient 5xx response is not enough to delete a resource. Mark
+it for review and provide repeatable evidence.
+
+## Local setup
+
+Install the exact Python version from `.python-version`, Ruby, Bundler, and the
+repository dependencies:
+
+```bash
+python -m venv .venv
+. .venv/bin/activate
+python -m pip install -r requirements-dev.lock.txt
+bundle install
+```
+
+## Required validation
+
+Run the same checks as CI before requesting review:
+
+```bash
+python -m pytest
+python tools/validate_catalog.py
+bundle exec jekyll build
+bundle exec ruby tests/render_security_test.rb
+```
+
+Maintainers can run the networked link audit from GitHub Actions with the
+scheduled **Catalog link audit** workflow. For a deliberate local audit of
+every catalog entry, including same-origin URLs:
+
+```bash
+python tools/check_links.py --mode all --output reports/link-check.json
+```
+
+Network link fetching is intentionally excluded from pull-request CI. A pull
+request can contain untrusted URLs, and status checks must remain deterministic.
+Any review-needed link result fails the scheduled audit so a maintainer must
+inspect it; it does not by itself justify removing the resource.
+
+## Pull request checklist
+
+- Keep catalog data and bilingual output consistent.
+- Include evidence for maintenance, license, safety, and adoption claims.
+- Do not call a project production-ready without current evidence.
+- Do not commit secrets, API keys, analytics IDs, generated reports, or local
+ build output.
+- Submit only material you have the right to submit and retain any required
+ third-party notice.
+- Explain user-visible changes and list the commands you ran.
+
+## Review and merge
+
+Maintainers may request edits, reclassify an entry, or decline resources that
+do not meet the curation policy. Passing automated checks does not replace
+editorial review. After merge, maintainers verify the GitHub Pages deployment
+and the production domain.
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..ede54ad
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,5 @@
+source "https://rubygems.org"
+
+ruby "3.3.4"
+
+gem "github-pages", "= 232", group: :jekyll_plugins
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..3f27afd
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,329 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ activesupport (8.1.3.1)
+ base64
+ bigdecimal
+ concurrent-ruby (~> 1.0, >= 1.3.1)
+ connection_pool (>= 2.2.5)
+ drb
+ i18n (>= 1.6, < 2)
+ json
+ logger (>= 1.4.2)
+ minitest (>= 5.1)
+ securerandom (>= 0.3)
+ tzinfo (~> 2.0, >= 2.0.5)
+ uri (>= 0.13.1)
+ addressable (2.9.0)
+ public_suffix (>= 2.0.2, < 8.0)
+ base64 (0.3.0)
+ bigdecimal (4.1.2)
+ coffee-script (2.4.1)
+ coffee-script-source
+ execjs
+ coffee-script-source (1.12.2)
+ colorator (1.1.0)
+ commonmarker (0.23.12)
+ concurrent-ruby (1.3.8)
+ connection_pool (3.0.2)
+ csv (3.3.6)
+ dnsruby (1.74.0)
+ base64 (>= 0.2)
+ logger (~> 1.6)
+ simpleidn (~> 0.2.1)
+ drb (2.2.3)
+ em-websocket (0.5.3)
+ eventmachine (>= 0.12.9)
+ http_parser.rb (~> 0)
+ ethon (0.18.0)
+ ffi (>= 1.15.0)
+ logger
+ eventmachine (1.2.7)
+ execjs (2.10.2)
+ json (>= 2)
+ faraday (2.14.3)
+ faraday-net_http (>= 2.0, < 3.5)
+ json
+ logger
+ faraday-net_http (3.4.4)
+ net-http (~> 0.5)
+ ffi (1.17.4)
+ ffi (1.17.4-aarch64-linux-gnu)
+ ffi (1.17.4-aarch64-linux-musl)
+ ffi (1.17.4-arm-linux-gnu)
+ ffi (1.17.4-arm-linux-musl)
+ ffi (1.17.4-arm64-darwin)
+ ffi (1.17.4-x86-linux-gnu)
+ ffi (1.17.4-x86-linux-musl)
+ ffi (1.17.4-x86_64-darwin)
+ ffi (1.17.4-x86_64-linux-gnu)
+ ffi (1.17.4-x86_64-linux-musl)
+ forwardable-extended (2.6.0)
+ gemoji (4.1.0)
+ github-pages (232)
+ github-pages-health-check (= 1.18.2)
+ jekyll (= 3.10.0)
+ jekyll-avatar (= 0.8.0)
+ jekyll-coffeescript (= 1.2.2)
+ jekyll-commonmark-ghpages (= 0.5.1)
+ jekyll-default-layout (= 0.1.5)
+ jekyll-feed (= 0.17.0)
+ jekyll-gist (= 1.5.0)
+ jekyll-github-metadata (= 2.16.1)
+ jekyll-include-cache (= 0.2.1)
+ jekyll-mentions (= 1.6.0)
+ jekyll-optional-front-matter (= 0.3.2)
+ jekyll-paginate (= 1.1.0)
+ jekyll-readme-index (= 0.3.0)
+ jekyll-redirect-from (= 0.16.0)
+ jekyll-relative-links (= 0.6.1)
+ jekyll-remote-theme (= 0.4.3)
+ jekyll-sass-converter (= 1.5.2)
+ jekyll-seo-tag (= 2.8.0)
+ jekyll-sitemap (= 1.4.0)
+ jekyll-swiss (= 1.0.0)
+ jekyll-theme-architect (= 0.2.0)
+ jekyll-theme-cayman (= 0.2.0)
+ jekyll-theme-dinky (= 0.2.0)
+ jekyll-theme-hacker (= 0.2.0)
+ jekyll-theme-leap-day (= 0.2.0)
+ jekyll-theme-merlot (= 0.2.0)
+ jekyll-theme-midnight (= 0.2.0)
+ jekyll-theme-minimal (= 0.2.0)
+ jekyll-theme-modernist (= 0.2.0)
+ jekyll-theme-primer (= 0.6.0)
+ jekyll-theme-slate (= 0.2.0)
+ jekyll-theme-tactile (= 0.2.0)
+ jekyll-theme-time-machine (= 0.2.0)
+ jekyll-titles-from-headings (= 0.5.3)
+ jemoji (= 0.13.0)
+ kramdown (= 2.4.0)
+ kramdown-parser-gfm (= 1.1.0)
+ liquid (= 4.0.4)
+ mercenary (~> 0.3)
+ minima (= 2.5.1)
+ nokogiri (>= 1.16.2, < 2.0)
+ rouge (= 3.30.0)
+ terminal-table (~> 1.4)
+ webrick (~> 1.8)
+ github-pages-health-check (1.18.2)
+ addressable (~> 2.3)
+ dnsruby (~> 1.60)
+ octokit (>= 4, < 8)
+ public_suffix (>= 3.0, < 6.0)
+ typhoeus (~> 1.3)
+ html-pipeline (2.14.3)
+ activesupport (>= 2)
+ nokogiri (>= 1.4)
+ http_parser.rb (0.8.1)
+ i18n (1.15.2)
+ concurrent-ruby (~> 1.0)
+ jekyll (3.10.0)
+ addressable (~> 2.4)
+ colorator (~> 1.0)
+ csv (~> 3.0)
+ em-websocket (~> 0.5)
+ i18n (>= 0.7, < 2)
+ jekyll-sass-converter (~> 1.0)
+ jekyll-watch (~> 2.0)
+ kramdown (>= 1.17, < 3)
+ liquid (~> 4.0)
+ mercenary (~> 0.3.3)
+ pathutil (~> 0.9)
+ rouge (>= 1.7, < 4)
+ safe_yaml (~> 1.0)
+ webrick (>= 1.0)
+ jekyll-avatar (0.8.0)
+ jekyll (>= 3.0, < 5.0)
+ jekyll-coffeescript (1.2.2)
+ coffee-script (~> 2.2)
+ coffee-script-source (~> 1.12)
+ jekyll-commonmark (1.4.0)
+ commonmarker (~> 0.22)
+ jekyll-commonmark-ghpages (0.5.1)
+ commonmarker (>= 0.23.7, < 1.1.0)
+ jekyll (>= 3.9, < 4.0)
+ jekyll-commonmark (~> 1.4.0)
+ rouge (>= 2.0, < 5.0)
+ jekyll-default-layout (0.1.5)
+ jekyll (>= 3.0, < 5.0)
+ jekyll-feed (0.17.0)
+ jekyll (>= 3.7, < 5.0)
+ jekyll-gist (1.5.0)
+ octokit (~> 4.2)
+ jekyll-github-metadata (2.16.1)
+ jekyll (>= 3.4, < 5.0)
+ octokit (>= 4, < 7, != 4.4.0)
+ jekyll-include-cache (0.2.1)
+ jekyll (>= 3.7, < 5.0)
+ jekyll-mentions (1.6.0)
+ html-pipeline (~> 2.3)
+ jekyll (>= 3.7, < 5.0)
+ jekyll-optional-front-matter (0.3.2)
+ jekyll (>= 3.0, < 5.0)
+ jekyll-paginate (1.1.0)
+ jekyll-readme-index (0.3.0)
+ jekyll (>= 3.0, < 5.0)
+ jekyll-redirect-from (0.16.0)
+ jekyll (>= 3.3, < 5.0)
+ jekyll-relative-links (0.6.1)
+ jekyll (>= 3.3, < 5.0)
+ jekyll-remote-theme (0.4.3)
+ addressable (~> 2.0)
+ jekyll (>= 3.5, < 5.0)
+ jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
+ rubyzip (>= 1.3.0, < 3.0)
+ jekyll-sass-converter (1.5.2)
+ sass (~> 3.4)
+ jekyll-seo-tag (2.8.0)
+ jekyll (>= 3.8, < 5.0)
+ jekyll-sitemap (1.4.0)
+ jekyll (>= 3.7, < 5.0)
+ jekyll-swiss (1.0.0)
+ jekyll-theme-architect (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-cayman (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-dinky (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-hacker (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-leap-day (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-merlot (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-midnight (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-minimal (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-modernist (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-primer (0.6.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-github-metadata (~> 2.9)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-slate (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-tactile (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-time-machine (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-titles-from-headings (0.5.3)
+ jekyll (>= 3.3, < 5.0)
+ jekyll-watch (2.2.1)
+ listen (~> 3.0)
+ jemoji (0.13.0)
+ gemoji (>= 3, < 5)
+ html-pipeline (~> 2.2)
+ jekyll (>= 3.0, < 5.0)
+ json (2.21.2)
+ kramdown (2.4.0)
+ rexml
+ kramdown-parser-gfm (1.1.0)
+ kramdown (~> 2.0)
+ liquid (4.0.4)
+ listen (3.10.0)
+ logger
+ rb-fsevent (~> 0.10, >= 0.10.3)
+ rb-inotify (~> 0.9, >= 0.9.10)
+ logger (1.7.0)
+ mercenary (0.3.6)
+ mini_portile2 (2.8.9)
+ minima (2.5.1)
+ jekyll (>= 3.5, < 5.0)
+ jekyll-feed (~> 0.9)
+ jekyll-seo-tag (~> 2.1)
+ minitest (6.0.6)
+ drb (~> 2.0)
+ prism (~> 1.5)
+ net-http (0.9.1)
+ uri (>= 0.11.1)
+ nokogiri (1.19.4)
+ mini_portile2 (~> 2.8.2)
+ racc (~> 1.4)
+ nokogiri (1.19.4-aarch64-linux-gnu)
+ racc (~> 1.4)
+ nokogiri (1.19.4-aarch64-linux-musl)
+ racc (~> 1.4)
+ nokogiri (1.19.4-arm-linux-gnu)
+ racc (~> 1.4)
+ nokogiri (1.19.4-arm-linux-musl)
+ racc (~> 1.4)
+ nokogiri (1.19.4-arm64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.19.4-x86_64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.19.4-x86_64-linux-gnu)
+ racc (~> 1.4)
+ nokogiri (1.19.4-x86_64-linux-musl)
+ racc (~> 1.4)
+ octokit (4.25.1)
+ faraday (>= 1, < 3)
+ sawyer (~> 0.9)
+ pathutil (0.16.2)
+ forwardable-extended (~> 2.6)
+ prism (1.9.0)
+ public_suffix (5.1.1)
+ racc (1.8.1)
+ rb-fsevent (0.11.2)
+ rb-inotify (0.11.1)
+ ffi (~> 1.0)
+ rexml (3.4.4)
+ rouge (3.30.0)
+ rubyzip (2.4.1)
+ safe_yaml (1.0.5)
+ sass (3.7.4)
+ sass-listen (~> 4.0.0)
+ sass-listen (4.0.0)
+ rb-fsevent (~> 0.9, >= 0.9.4)
+ rb-inotify (~> 0.9, >= 0.9.7)
+ sawyer (0.9.3)
+ addressable (>= 2.3.5)
+ faraday (>= 0.17.3, < 3)
+ securerandom (0.4.1)
+ simpleidn (0.2.3)
+ terminal-table (1.8.0)
+ unicode-display_width (~> 1.1, >= 1.1.1)
+ typhoeus (1.6.0)
+ ethon (>= 0.18.0)
+ tzinfo (2.0.6)
+ concurrent-ruby (~> 1.0)
+ unicode-display_width (1.8.0)
+ uri (1.1.1)
+ webrick (1.9.2)
+
+PLATFORMS
+ aarch64-linux-gnu
+ aarch64-linux-musl
+ arm-linux-gnu
+ arm-linux-musl
+ arm64-darwin
+ arm64-darwin-25
+ ruby
+ x86-linux-gnu
+ x86-linux-musl
+ x86_64-darwin
+ x86_64-linux
+ x86_64-linux-gnu
+ x86_64-linux-musl
+
+DEPENDENCIES
+ github-pages (= 232)
+
+RUBY VERSION
+ ruby 3.3.4p94
+
+BUNDLED WITH
+ 2.5.11
diff --git a/README.md b/README.md
index 47f8cbc..116584f 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,49 @@
-python
-======
-学习Python时的代码
+# FlyPython Python Resource Catalog
+[](https://github.com/flypythoncom/python/stargazers)
+[](https://github.com/flypythoncom/python/forks)
+[](https://python.flypython.com/)
+[English](README.md) · [中文](README_cn.md)
+FlyPython is the broad, community-maintained resource catalog behind the
+[FlyPython learning hub](https://flypython.com/). It keeps the larger source map
+public and reviewable while the main site provides editorial learning paths and a
+smaller featured collection.
+## Start here
-####2016-03-20 update
+- [Follow the practical Python roadmap](https://flypython.com/learn)
+- [Browse the featured resource index](https://flypython.com/resources)
+- [Build and test a no-key Python agent loop](https://flypython.com/learn/python-ai-agent-roadmap)
+- [Explore the full community catalog](https://python.flypython.com/)
-[Machine Learning with Python](https://github.com/xxg1413/MachineLearning)
-- [Building Machine Learning Systems](https://github.com/xxg1413/MachineLearning/tree/master/Building%20Machine%20Learning%20Systems)
-- [CS229 Machine Learning](https://github.com/xxg1413/MachineLearning/tree/master/CS229%20Machine%20Learning)
-- [Machine Learning in Action](https://github.com/xxg1413/MachineLearning/tree/master/Machine%20Learning%20in%20Action)
-- [NumPy Beginner's Guide](https://github.com/xxg1413/MachineLearning/tree/master/NumPy%20Beginner's%20Guide)
-- [Web Scraping with Python](https://github.com/xxg1413/MachineLearning/tree/master/Web%20Scraping%20with%20Python)
+## Four paths
+1. **Python foundations** — language, environments, dependencies, typing, and tests
+2. **Web and APIs** — typed services, validation, HTTP clients, and applications
+3. **Automation** — files, processes, browsers, crawling, and data workflows
+4. **AI agents** — tools, structured output, state, evaluation, and safety boundaries
+The canonical catalog lives in [`_data/resources.yml`](_data/resources.yml).
+The English and Chinese site pages read from that same file; the full resource
+list is not copied into the README files.
+## Quality bar
+
+- Prefer official documentation, standards, and first-party project sources.
+- Explain why each resource matters instead of publishing an unranked link dump.
+- Record the review date, API-key requirement, and relevant safety boundary.
+- Label experiments honestly; do not infer production readiness from popularity.
+
+See [CONTRIBUTING.md](CONTRIBUTING.md) before proposing a resource or changing
+its classification.
+
+## Local preview
+
+```bash
+bundle install
+bundle exec jekyll serve
+```
+
+The catalog is published at [python.flypython.com](https://python.flypython.com/).
diff --git a/README_cn.md b/README_cn.md
new file mode 100644
index 0000000..27a58dd
--- /dev/null
+++ b/README_cn.md
@@ -0,0 +1,45 @@
+# FlyPython Python 资源目录
+
+[](https://github.com/flypythoncom/python/stargazers)
+[](https://github.com/flypythoncom/python/forks)
+[](https://python.flypython.com/zh/)
+
+[English](README.md) · [中文](README_cn.md)
+
+FlyPython 是 [FlyPython 学习站](https://flypython.com/)背后的社区公开资源目录。
+本仓库维护覆盖面更广、可公开审核的来源地图;主站负责编辑型学习路径和更精简的精选资源。
+
+## 从这里开始
+
+- [查看实用 Python 学习路线](https://flypython.com/learn)
+- [浏览主站精选资源](https://flypython.com/resources)
+- [构建并测试无需 API Key 的 Python Agent 循环](https://flypython.com/learn/python-ai-agent-roadmap)
+- [浏览完整中文目录](https://python.flypython.com/zh/)
+
+## 四条路径
+
+1. **Python 基础** — 语言、环境、依赖、类型与测试
+2. **Web 与 API** — 类型化服务、数据验证、HTTP 客户端与应用
+3. **自动化** — 文件、进程、浏览器、爬取与数据工作流
+4. **AI Agent** — 工具、结构化输出、状态、评测与安全边界
+
+唯一的目录数据源是 [`_data/resources.yml`](_data/resources.yml)。英文页和中文页
+都读取同一份数据,不再在多个 Markdown 文件中复制完整资源清单。
+
+## 收录标准
+
+- 优先选择官方文档、正式标准与项目的一手来源。
+- 说明每条资源为什么值得使用,而不是堆放未经筛选的链接。
+- 记录审核日期、API Key 要求与需要关注的安全边界。
+- 如实标记实验性内容,不用流行度推断“生产可用”。
+
+提交新资源或修改分类前,请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。
+
+## 本地预览
+
+```bash
+bundle install
+bundle exec jekyll serve
+```
+
+目录发布在 [python.flypython.com/zh/](https://python.flypython.com/zh/)。
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..7fd314e
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,48 @@
+# Security policy
+
+## Supported versions
+
+Security fixes apply to the default branch and the currently deployed
+`python.flypython.com` site. Historical commits, forks, and archived catalog
+entries are not supported releases.
+
+## Report a vulnerability privately
+
+Do not open a public issue for a security vulnerability. Use
+[GitHub's private vulnerability reporting](https://github.com/flypythoncom/python/security/advisories/new).
+If that form is unavailable, email hello@flypython.com with the subject
+`FlyPython security report`.
+
+Include:
+
+- The affected URL, file, workflow, or commit
+- Clear reproduction steps and impact
+- Any proof of concept needed to confirm the issue
+- Whether the issue is already public
+- A safe way to contact you
+
+Remove credentials and personal data that are not required to reproduce the
+issue. We will acknowledge reports on a best-effort basis, investigate, and
+coordinate disclosure after a fix is available.
+
+## In scope
+
+Examples include:
+
+- Script injection or unsafe rendered catalog content
+- A link-checking path that can reach private, loopback, or metadata services
+- Workflow permission escalation or untrusted-code execution
+- Exposed credentials, DNS takeover, or custom-domain control issues
+- Dependency or build-chain compromise with a demonstrated impact
+
+Broken links, outdated descriptions, ordinary 403/429 responses, and resource
+quality disagreements are not security vulnerabilities. Use the broken-link or
+resource-proposal issue form for those reports.
+
+## Safe harbor
+
+Make a good-faith effort to avoid privacy violations, service disruption, data
+destruction, and access beyond what is needed to demonstrate the issue. Do not
+perform denial-of-service testing or interact with third-party resources beyond
+their published policies. We will not pursue action against good-faith research
+that follows this policy.
diff --git a/_config.yml b/_config.yml
new file mode 100644
index 0000000..075862b
--- /dev/null
+++ b/_config.yml
@@ -0,0 +1,73 @@
+title: FlyPython Resource Catalog
+description: >-
+ A reviewed catalog of primary Python sources for foundations, web APIs,
+ automation, and AI agents.
+url: "https://python.flypython.com"
+baseurl: ""
+repository: flypythoncom/python
+timezone: Asia/Shanghai
+
+author:
+ name: FlyPython
+ url: https://flypython.com/
+ email: hello@flypython.com
+ github: flypythoncom
+
+lang: en-US
+logo: /assets/images/logo.svg
+
+social:
+ name: FlyPython
+ links:
+ - https://flypython.com/
+ - https://github.com/flypythoncom
+
+markdown: kramdown
+highlighter: rouge
+permalink: pretty
+plugins:
+ - jekyll-sitemap
+ - jekyll-seo-tag
+
+optional_front_matter:
+ enabled: false
+
+readme_index:
+ enabled: false
+
+relative_links:
+ enabled: false
+
+titles_from_headings:
+ enabled: false
+
+kramdown:
+ input: GFM
+ hard_wrap: false
+ syntax_highlighter: rouge
+
+exclude:
+ - .github/
+ - .gitignore
+ - .python-version
+ - .ruby-version
+ - AGENTS.md
+ - CHANGELOG.md
+ - CODE_OF_CONDUCT.md
+ - CONTRIBUTING.md
+ - README.md
+ - README_cn.md
+ - SECURITY.md
+ - Gemfile
+ - Gemfile.lock
+ - docs/
+ - pyproject.toml
+ - requirements-dev.lock.txt
+ - requirements-dev.txt
+ - requirements.lock.txt
+ - requirements.txt
+ - node_modules/
+ - vendor/
+ - tools/
+ - tests/
+ - reports/
diff --git a/_data/resources.yml b/_data/resources.yml
new file mode 100644
index 0000000..4798af9
--- /dev/null
+++ b/_data/resources.yml
@@ -0,0 +1,344 @@
+catalog:
+ reviewed_on: "2026-08-31"
+ status: active
+ paths:
+ - id: foundations
+ order: 1
+ title_en: "Python foundations"
+ title_zh: "Python 基础"
+ summary_en: "Learn the language, environments, dependencies, typing, and tests that reliable Python work depends on."
+ summary_zh: "掌握语言基础、环境、依赖、类型与测试,为可靠的 Python 项目打好根基。"
+ - id: web-apis
+ order: 2
+ title_en: "Web and APIs"
+ title_zh: "Web 与 API"
+ summary_en: "Build typed services and applications that connect Python logic to users and other systems."
+ summary_zh: "构建有类型约束的服务和应用,把 Python 逻辑连接到用户与其他系统。"
+ - id: automation
+ order: 3
+ title_en: "Automation"
+ title_zh: "自动化"
+ summary_en: "Turn repeatable work into maintainable scripts, browser workflows, and data pipelines."
+ summary_zh: "把重复工作变成可维护的脚本、浏览器工作流和数据管道。"
+ - id: ai-agents
+ order: 4
+ title_en: "AI agents"
+ title_zh: "AI Agent"
+ summary_en: "Learn tools, structured output, state, evaluation, and the safety boundaries of agent systems."
+ summary_zh: "理解工具调用、结构化输出、状态、评测以及 Agent 系统的安全边界。"
+
+resources:
+ - id: python-tutorial
+ path: foundations
+ title: "The Python Tutorial"
+ url: "https://docs.python.org/3/tutorial/"
+ source_type: official-docs
+ level: beginner
+ language: en
+ why_en: "The primary introduction to Python syntax, data structures, modules, errors, classes, and the standard library."
+ why_zh: "Python 官方入门教程,覆盖语法、数据结构、模块、异常、类与标准库。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: low
+ featured: true
+
+ - id: python-venv
+ path: foundations
+ title: "venv — Creation of virtual environments"
+ url: "https://docs.python.org/3/library/venv.html"
+ source_type: official-docs
+ level: beginner
+ language: en
+ why_en: "The standard-library reference for creating isolated Python environments and understanding their behavior."
+ why_zh: "Python 标准库的虚拟环境参考,说明如何创建隔离环境及其运行方式。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: low
+ featured: false
+
+ - id: python-packaging-guide
+ path: foundations
+ title: "Python Packaging User Guide"
+ url: "https://packaging.python.org/en/latest/tutorials/packaging-projects/"
+ source_type: official-docs
+ level: intermediate
+ language: en
+ why_en: "The PyPA tutorial for project layout, build metadata, distributions, and publishing Python packages."
+ why_zh: "PyPA 官方教程,覆盖项目结构、构建元数据、分发包与 Python 包发布。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: low
+ featured: false
+
+ - id: uv-docs
+ path: foundations
+ title: "uv documentation"
+ url: "https://docs.astral.sh/uv/"
+ source_type: official-project
+ level: all-levels
+ language: en
+ why_en: "A modern workflow for Python versions, virtual environments, dependencies, lockfiles, and project commands."
+ why_zh: "现代 Python 工作流,统一管理版本、虚拟环境、依赖、锁文件和项目命令。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: low
+ featured: true
+
+ - id: pytest-docs
+ path: foundations
+ title: "pytest documentation"
+ url: "https://docs.pytest.org/en/stable/"
+ source_type: official-project
+ level: intermediate
+ language: en
+ why_en: "The official guide to test discovery, assertions, fixtures, parametrization, and maintainable test suites."
+ why_zh: "pytest 官方指南,覆盖测试发现、断言、fixture、参数化和可维护测试套件。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: low
+ featured: false
+
+ - id: typing-docs
+ path: foundations
+ title: "Python typing documentation"
+ url: "https://typing.python.org/en/latest/"
+ source_type: official-docs
+ level: intermediate
+ language: en
+ why_en: "The canonical home for Python's type-system specification, guides, and typing best practices."
+ why_zh: "Python 类型系统规范、指南与类型标注实践的权威入口。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: low
+ featured: false
+
+ - id: fastapi-docs
+ path: web-apis
+ title: "FastAPI documentation"
+ url: "https://fastapi.tiangolo.com/"
+ source_type: official-docs
+ level: intermediate
+ language: en
+ why_en: "A direct route from typed Python functions to tested APIs with validation and generated documentation."
+ why_zh: "从带类型的 Python 函数出发,构建具备验证、测试和自动文档的 API。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: low
+ featured: true
+
+ - id: django-tutorial
+ path: web-apis
+ title: "Django documentation: First steps"
+ url: "https://docs.djangoproject.com/en/stable/intro/tutorial01/"
+ source_type: official-docs
+ level: beginner
+ language: en
+ why_en: "The official path through Django projects, models, views, templates, forms, tests, and reusable apps."
+ why_zh: "Django 官方入门路径,覆盖项目、模型、视图、模板、表单、测试和可复用应用。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: low
+ featured: false
+
+ - id: flask-docs
+ path: web-apis
+ title: "Flask documentation"
+ url: "https://flask.palletsprojects.com/en/stable/"
+ source_type: official-docs
+ level: beginner
+ language: en
+ why_en: "The maintained reference and tutorial for Flask applications, requests, templates, testing, and deployment."
+ why_zh: "Flask 官方参考与教程,覆盖应用、请求、模板、测试和部署。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: low
+ featured: false
+
+ - id: pydantic-docs
+ path: web-apis
+ title: "Pydantic documentation"
+ url: "https://pydantic.dev/docs/validation/latest/get-started/"
+ source_type: official-docs
+ level: intermediate
+ language: en
+ why_en: "The primary guide to validating untrusted data and expressing typed contracts at Python system boundaries."
+ why_zh: "在 Python 系统边界验证不可信数据、定义类型契约的官方指南。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: low
+ featured: false
+
+ - id: httpx-docs
+ path: web-apis
+ title: "HTTPX documentation"
+ url: "https://www.python-httpx.org/"
+ source_type: official-project
+ level: intermediate
+ language: en
+ why_en: "The official reference for synchronous and asynchronous HTTP clients, timeouts, streaming, and transport control."
+ why_zh: "同步与异步 HTTP 客户端、超时、流式响应和传输控制的官方参考。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: low
+ featured: false
+
+ - id: pathlib-docs
+ path: automation
+ title: "pathlib — Object-oriented filesystem paths"
+ url: "https://docs.python.org/3/library/pathlib.html"
+ source_type: official-docs
+ level: beginner
+ language: en
+ why_en: "The standard-library reference for readable, cross-platform file and directory automation."
+ why_zh: "使用可读、跨平台的对象式路径完成文件与目录自动化的标准库参考。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: low
+ featured: false
+
+ - id: subprocess-docs
+ path: automation
+ title: "subprocess — Subprocess management"
+ url: "https://docs.python.org/3/library/subprocess.html"
+ source_type: official-docs
+ level: intermediate
+ language: en
+ why_en: "The standard-library contract for launching processes, capturing output, handling failures, and avoiding unsafe shell usage."
+ why_zh: "启动进程、捕获输出、处理失败并避免不安全 shell 用法的标准库契约。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: medium
+ featured: false
+
+ - id: playwright-python
+ path: automation
+ title: "Playwright for Python"
+ url: "https://playwright.dev/python/docs/intro"
+ source_type: official-docs
+ level: intermediate
+ language: en
+ why_en: "The official Python guide to reliable browser automation, locators, assertions, traces, and isolated contexts."
+ why_zh: "可靠浏览器自动化、定位器、断言、追踪和隔离上下文的 Python 官方指南。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: medium
+ featured: false
+
+ - id: scrapy-tutorial
+ path: automation
+ title: "Scrapy tutorial"
+ url: "https://docs.scrapy.org/en/latest/intro/tutorial.html"
+ source_type: official-docs
+ level: intermediate
+ language: en
+ why_en: "The official introduction to crawl structure, extraction, item pipelines, persistence, and responsible operation."
+ why_zh: "爬虫结构、数据提取、item pipeline、持久化与负责任运行的官方入门。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: medium
+ featured: false
+
+ - id: pandas-getting-started
+ path: automation
+ title: "pandas getting started guides"
+ url: "https://pandas.pydata.org/docs/getting_started/index.html"
+ source_type: official-docs
+ level: beginner
+ language: en
+ why_en: "The primary entry point for tabular data loading, cleaning, transformation, analysis, and export."
+ why_zh: "表格数据读取、清洗、转换、分析和导出的官方入门入口。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: low
+ featured: false
+
+ - id: openai-agents-sdk
+ path: ai-agents
+ title: "OpenAI Agents SDK"
+ url: "https://openai.github.io/openai-agents-python/"
+ source_type: official-docs
+ level: intermediate
+ language: en
+ why_en: "The official Python toolkit for agents, tools, handoffs, guardrails, sessions, tracing, and orchestration."
+ why_zh: "面向 Agent、工具、handoff、guardrail、session、追踪与编排的官方 Python 工具包。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: true
+ risk: medium
+ featured: true
+
+ - id: pydantic-ai
+ path: ai-agents
+ title: "Pydantic AI"
+ url: "https://pydantic.dev/docs/ai/overview/"
+ source_type: official-docs
+ level: intermediate
+ language: en
+ why_en: "Typed agent development built around validated inputs, structured output, tools, testing, and model portability."
+ why_zh: "以输入验证、结构化输出、工具、测试和模型可移植性为核心的类型安全 Agent 开发。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: true
+ risk: medium
+ featured: true
+
+ - id: model-context-protocol
+ path: ai-agents
+ title: "Model Context Protocol"
+ url: "https://modelcontextprotocol.io/docs/getting-started/intro"
+ source_type: official-standard
+ level: intermediate
+ language: en
+ why_en: "The open protocol and reference documentation for connecting AI applications to tools, data, and reusable context."
+ why_zh: "连接 AI 应用、工具、数据与可复用上下文的开放协议和权威参考。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: false
+ risk: medium
+ featured: true
+
+ - id: langgraph-python
+ path: ai-agents
+ title: "LangGraph overview"
+ url: "https://docs.langchain.com/oss/python/langgraph/overview"
+ source_type: official-docs
+ level: advanced
+ language: en
+ why_en: "The official Python guide to stateful graph workflows, durable execution, memory, interrupts, and human review."
+ why_zh: "有状态图工作流、持久执行、记忆、中断与人工审核的 Python 官方指南。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: true
+ risk: medium
+ featured: false
+
+ - id: google-adk
+ path: ai-agents
+ title: "Google Agent Development Kit"
+ url: "https://adk.dev/"
+ source_type: official-docs
+ level: advanced
+ language: en
+ why_en: "Google's official documentation for developing, evaluating, and deploying modular agent systems."
+ why_zh: "Google 官方模块化 Agent 系统开发、评测与部署文档。"
+ reviewed_on: "2026-08-31"
+ status: active
+ requires_key: true
+ risk: medium
+ featured: false
diff --git a/_includes/catalog-grid.html b/_includes/catalog-grid.html
new file mode 100644
index 0000000..001c694
--- /dev/null
+++ b/_includes/catalog-grid.html
@@ -0,0 +1,83 @@
+{% assign ordered_paths = site.data.resources.catalog.paths | sort: "order" %}
+
+
+{% for path in ordered_paths %}
+ {% assign path_resources = include.resources | where: "path", path.id %}
+
+
+
+ {% if page.lang == "zh-CN" %}路径{% else %}Path{% endif %} 0{{ path.order | escape }}
+
{% if page.lang == "zh-CN" %}{{ path.title_zh | escape }}{% else %}{{ path.title_en | escape }}{% endif %}
+
+ {% if page.lang == "zh-CN" %}{{ path.summary_zh | escape }}{% else %}{{ path.summary_en | escape }}{% endif %}
+
+
+
+ {% for resource in path_resources %}
+
+
+
+ {% if page.lang == "zh-CN" %}{{ resource.why_zh | escape }}{% else %}{{ resource.why_en | escape }}{% endif %}
+
+ {% if resource.requires_key or resource.risk == "medium" %}
+
+ {% if resource.requires_key %}{% if page.lang == "zh-CN" %}典型用法需要 API Key{% else %}API key required for typical use{% endif %}{% endif %}
+ {% if resource.requires_key and resource.risk == "medium" %}·{% endif %}
+ {% if resource.risk == "medium" %}{% if page.lang == "zh-CN" %}请检查权限与外部副作用{% else %}Review permissions and side effects{% endif %}{% endif %}
+
+ {% endif %}
+
+ {% endfor %}
+
+
+{% endfor %}
diff --git a/_includes/head-custom.html b/_includes/head-custom.html
new file mode 100644
index 0000000..4958d72
--- /dev/null
+++ b/_includes/head-custom.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+{% if page.lang == 'zh-CN' %}
+
+{% else %}
+
+{% endif %}
+
diff --git a/_layouts/default.html b/_layouts/default.html
new file mode 100644
index 0000000..14b52df
--- /dev/null
+++ b/_layouts/default.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+ {% seo %}
+
+ {% include head-custom.html %}
+
+ {% assign active_resources = site.data.resources.resources | where: "status", "active" %}
+
+ {% if page.lang == 'zh-CN' %}
+ 跳到主要内容
+ {% else %}
+ Skip to main content
+ {% endif %}
+
+
+
+
+ {{ content }}
+
+
+
+
+
diff --git a/assets/css/custom.css b/assets/css/custom.css
new file mode 100644
index 0000000..8e576c6
--- /dev/null
+++ b/assets/css/custom.css
@@ -0,0 +1,540 @@
+:root {
+ --ink: #172a35;
+ --muted: #5b6d77;
+ --line: #d8e3e4;
+ --surface: #ffffff;
+ --surface-soft: #f2f8f8;
+ --brand: #0f6f70;
+ --brand-dark: #0a4f50;
+ --brand-soft: #dff1f0;
+ --accent: #f0b429;
+ --caution: #7b4c00;
+ --caution-bg: #fff6df;
+ --shadow: 0 18px 50px rgba(21, 55, 65, 0.08);
+ --content-width: 72rem;
+}
+* {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ html {
+ scroll-behavior: auto;
+ }
+}
+
+body {
+ margin: 0;
+ color: var(--ink);
+ background: var(--surface);
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ line-height: 1.65;
+ text-rendering: optimizeLegibility;
+}
+
+a {
+ color: var(--brand);
+ text-underline-offset: 0.18em;
+}
+
+a:hover {
+ color: var(--brand-dark);
+}
+
+a:focus-visible,
+button:focus-visible {
+ outline: 3px solid var(--accent);
+ outline-offset: 3px;
+}
+
+.skip-link {
+ position: fixed;
+ top: 0.75rem;
+ left: 0.75rem;
+ z-index: 100;
+ padding: 0.65rem 0.9rem;
+ color: #ffffff;
+ background: var(--brand-dark);
+ border-radius: 0.4rem;
+ transform: translateY(-180%);
+}
+
+.skip-link:focus {
+ color: #ffffff;
+ transform: translateY(0);
+}
+
+.site-header {
+ color: #ffffff;
+ background:
+ radial-gradient(circle at 86% 18%, rgba(255, 255, 255, 0.14), transparent 22rem),
+ linear-gradient(135deg, var(--brand) 0%, #183d4b 100%);
+}
+
+.site-header-inner,
+.main-content,
+.site-footer-inner {
+ width: min(calc(100% - 2rem), var(--content-width));
+ margin-inline: auto;
+}
+
+.site-header-inner {
+ padding: 1.4rem 0 2rem;
+}
+
+.brand-row,
+.header-summary,
+.site-footer-inner {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1.5rem;
+}
+
+.site-brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.75rem;
+ color: #ffffff;
+ font-size: 1.12rem;
+ font-weight: 750;
+ text-decoration: none;
+}
+
+.site-brand:hover {
+ color: #ffffff;
+}
+
+.site-brand small {
+ margin-left: 0.32rem;
+ color: rgba(255, 255, 255, 0.72);
+ font-size: 0.78rem;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+.brand-mark {
+ display: grid;
+ width: 2.4rem;
+ height: 2.4rem;
+ place-items: center;
+ color: var(--brand-dark);
+ background: #ffffff;
+ border-radius: 0.7rem;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ font-weight: 800;
+}
+
+.language-nav {
+ display: inline-flex;
+ padding: 0.22rem;
+ background: rgba(255, 255, 255, 0.12);
+ border: 1px solid rgba(255, 255, 255, 0.22);
+ border-radius: 999px;
+}
+
+.language-nav a {
+ display: inline-flex;
+ min-height: 2.75rem;
+ align-items: center;
+ padding: 0.35rem 0.7rem;
+ color: rgba(255, 255, 255, 0.84);
+ border-radius: 999px;
+ font-size: 0.84rem;
+ text-decoration: none;
+}
+
+.language-nav a[aria-current="page"] {
+ color: var(--brand-dark);
+ background: #ffffff;
+ font-weight: 700;
+}
+
+.header-summary {
+ align-items: flex-end;
+ margin-top: 2rem;
+}
+
+.header-summary > p {
+ max-width: 39rem;
+ margin: 0;
+ color: rgba(255, 255, 255, 0.9);
+ font-size: clamp(1rem, 2vw, 1.18rem);
+}
+
+.header-summary ul {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 0.35rem 1rem;
+ margin: 0;
+ padding: 0;
+ color: rgba(255, 255, 255, 0.78);
+ font-size: 0.8rem;
+ list-style: none;
+}
+
+.header-summary a {
+ color: #ffffff;
+}
+
+.main-content {
+ padding: clamp(2.5rem, 7vw, 5.25rem) 0;
+}
+
+.main-content > h1 {
+ max-width: 50rem;
+ margin: 0 0 1rem;
+ color: var(--ink);
+ font-size: clamp(2.35rem, 7vw, 4.75rem);
+ line-height: 1.04;
+ letter-spacing: -0.045em;
+}
+
+.main-content > h1 + p {
+ max-width: 48rem;
+ margin: 0;
+ color: var(--muted);
+ font-size: clamp(1.03rem, 2vw, 1.22rem);
+}
+
+.main-content > h2 {
+ margin-top: 4.5rem;
+ font-size: clamp(1.7rem, 4vw, 2.35rem);
+ letter-spacing: -0.025em;
+}
+
+.cta-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ margin: 2rem 0 1.4rem;
+}
+
+.button {
+ display: inline-flex;
+ min-height: 2.75rem;
+ align-items: center;
+ justify-content: center;
+ padding: 0.65rem 1rem;
+ color: var(--brand-dark);
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: 0.65rem;
+ font-weight: 700;
+ text-decoration: none;
+}
+
+.button:hover {
+ color: var(--brand-dark);
+ background: var(--surface-soft);
+ border-color: #acc8c9;
+}
+
+.button-primary,
+.button-primary:hover {
+ color: #ffffff;
+ background: var(--brand);
+ border-color: var(--brand);
+}
+
+.catalog-note {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem 0.55rem;
+ margin: 0;
+ color: var(--muted);
+ font-size: 0.88rem;
+}
+
+.catalog-note strong {
+ color: var(--ink);
+}
+
+.path-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 0.9rem;
+ margin: 3.5rem 0 5rem;
+}
+
+.path-card {
+ display: flex;
+ min-height: 15.5rem;
+ flex-direction: column;
+ padding: 1.25rem;
+ color: var(--ink);
+ background: linear-gradient(180deg, #ffffff 0%, var(--surface-soft) 100%);
+ border: 1px solid var(--line);
+ border-radius: 1rem;
+ box-shadow: var(--shadow);
+ text-decoration: none;
+ transition: transform 160ms ease, border-color 160ms ease;
+}
+
+.path-card:hover {
+ color: var(--ink);
+ border-color: #9dbfc0;
+ transform: translateY(-3px);
+}
+
+.path-number,
+.section-eyebrow {
+ color: var(--brand);
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ font-size: 0.76rem;
+ font-weight: 800;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.path-card strong {
+ margin: 1.6rem 0 0.55rem;
+ font-size: 1.15rem;
+ line-height: 1.3;
+}
+
+.path-card > span:not(.path-number) {
+ color: var(--muted);
+ font-size: 1rem;
+}
+
+.path-card small {
+ margin-top: auto;
+ padding-top: 1rem;
+ color: var(--brand);
+ font-weight: 700;
+}
+
+.path-section {
+ margin: 5rem 0;
+ scroll-margin-top: 1.5rem;
+}
+
+.section-heading {
+ display: grid;
+ grid-template-columns: minmax(14rem, 0.75fr) minmax(18rem, 1.25fr);
+ gap: 2rem;
+ align-items: end;
+ margin-bottom: 1.5rem;
+ padding-bottom: 1.2rem;
+ border-bottom: 1px solid var(--line);
+}
+
+.section-heading h2 {
+ margin: 0.3rem 0 0;
+ font-size: clamp(1.75rem, 4vw, 2.5rem);
+ letter-spacing: -0.03em;
+}
+
+.section-heading > p {
+ margin: 0;
+ color: var(--muted);
+}
+
+.resource-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 1rem;
+}
+
+.resource-card {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ padding: 1.25rem;
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: 0.9rem;
+}
+
+.resource-card.featured {
+ border-color: #86b8b9;
+ box-shadow: inset 0 3px 0 var(--brand);
+}
+
+.resource-card-header {
+ display: flex;
+ justify-content: space-between;
+ gap: 0.75rem;
+}
+
+.source-badge,
+.featured-badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.16rem 0.48rem;
+ border-radius: 999px;
+ font-size: 0.7rem;
+ font-weight: 800;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+.source-badge {
+ color: var(--brand-dark);
+ background: var(--brand-soft);
+}
+
+.featured-badge {
+ color: #6d4600;
+ background: #fff0bd;
+}
+
+.resource-card h3 {
+ margin: 1rem 0 0.5rem;
+ font-size: 1.15rem;
+ line-height: 1.35;
+}
+
+.resource-card h3 a {
+ color: var(--ink);
+ text-decoration-thickness: 1px;
+ text-decoration-color: #a9c8c9;
+}
+
+.resource-card h3 a::after {
+ content: " ↗";
+ color: var(--brand);
+ font-size: 0.78em;
+}
+
+.resource-card > p:not(.resource-caution) {
+ margin: 0;
+ color: var(--muted);
+ font-size: 1rem;
+}
+
+.resource-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.3rem 0.8rem;
+ margin: auto 0 0;
+ padding: 1.2rem 0 0;
+ color: var(--muted);
+ font-size: 0.74rem;
+ list-style: none;
+ text-transform: capitalize;
+}
+
+.resource-meta li:not(:first-child)::before {
+ content: "·";
+ margin-right: 0.8rem;
+ color: #9aa9ae;
+}
+
+.resource-caution {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem 0.45rem;
+ margin: 0.85rem 0 0;
+ padding: 0.55rem 0.7rem;
+ color: var(--caution);
+ background: var(--caution-bg);
+ border-radius: 0.5rem;
+ font-size: 1rem;
+}
+
+.site-footer {
+ padding: 2rem 0;
+ color: var(--muted);
+ background: var(--surface-soft);
+ border-top: 1px solid var(--line);
+}
+
+.site-footer p {
+ margin: 0;
+}
+
+.site-footer nav {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+}
+
+.site-footer a {
+ font-weight: 650;
+}
+
+@media (max-width: 58rem) {
+ .path-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .path-card {
+ min-height: 13rem;
+ }
+}
+
+@media (max-width: 42rem) {
+ .brand-row,
+ .header-summary,
+ .site-footer-inner {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .site-brand small {
+ display: block;
+ margin: 0;
+ }
+
+ .header-summary ul {
+ justify-content: flex-start;
+ }
+
+ .path-grid,
+ .resource-grid,
+ .section-heading {
+ grid-template-columns: 1fr;
+ }
+
+ .path-grid {
+ margin-bottom: 4rem;
+ }
+
+ .path-card {
+ min-height: 0;
+ }
+
+ .section-heading {
+ gap: 0.75rem;
+ }
+
+ .cta-row .button {
+ width: 100%;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ html {
+ scroll-behavior: auto;
+ }
+
+ .path-card {
+ transition: none;
+ }
+}
+
+@media print {
+ .site-header,
+ .site-footer,
+ .cta-row,
+ .path-grid {
+ display: none;
+ }
+
+ .main-content {
+ width: 100%;
+ padding: 0;
+ }
+
+ .resource-card {
+ break-inside: avoid;
+ }
+}
diff --git a/assets/images/logo.svg b/assets/images/logo.svg
new file mode 100644
index 0000000..b57e2a9
--- /dev/null
+++ b/assets/images/logo.svg
@@ -0,0 +1,18 @@
+
diff --git a/assets/images/og-image.png b/assets/images/og-image.png
new file mode 100644
index 0000000..1709d98
Binary files /dev/null and b/assets/images/og-image.png differ
diff --git a/assets/images/og-image.svg b/assets/images/og-image.svg
new file mode 100644
index 0000000..da12ac7
--- /dev/null
+++ b/assets/images/og-image.svg
@@ -0,0 +1,28 @@
+
diff --git a/datastruct/binarysearch.py b/datastruct/binarysearch.py
deleted file mode 100644
index fa6a1e2..0000000
--- a/datastruct/binarysearch.py
+++ /dev/null
@@ -1,25 +0,0 @@
-def BinarySearch(l,key):
- low=0
- high=len(l)-1
- i=0
- while(low <= high):
- i = i+1
- mid = low + ((high-low)>>1)
- if(l[mid] < key):
- low = mid + 1
- elif (l[mid] > key):
- high = mid -1
- else:
- print "use %d times" % i
- return mid
- return -1
-
-if __name__ == "__main__":
- l=[1,4,5,6,7,8,9,44,333,2233]
- print l
- print BinarySearch(l,4)
- print BinarySearch(l,44)
- print BinarySearch(l,8)
- print BinarySearch(l,2233)
- print BinarySearch(l,77)
-
diff --git a/datastruct/btree.py b/datastruct/btree.py
deleted file mode 100644
index 69434e8..0000000
--- a/datastruct/btree.py
+++ /dev/null
@@ -1,62 +0,0 @@
-class BTree:
- def __init__(self,value):
- self.left=None
- self.data=value
- self.right=None
-
- def insertLeft(self,value):
- self.left=BTree(value)
- return self.left
-
- def insertRight(self,value):
- self.right=BTree(value)
- return self.right
-
- def show(self):
- print self.data
-
-def preorder(node):
- if node.data:
- node.show()
- if node.left:
- preorder(node.left)
- if node.right:
- preorder(node.right)
-
-def inorder(node):
- if node.data:
- if node.left:
- inorder(node.left)
- node.show()
- if node.right:
- inorder(node.right)
-
-def postorder(node):
- if node.data:
- if node.left:
- postorder(node.left)
- if node.right:
- postorder(node.right)
- node.show()
-
-if __name__ == "__main__":
-
- Root=BTree("root")
- A=Root.insertLeft("A")
- C=A.insertLeft("C")
- D=C.insertRight("D")
- F=D.insertLeft("F")
- G=D.insertRight("G")
- B=Root.insertRight("B")
- E=B.insertRight("E")
-
- print "pre-traversal"
- preorder(Root)
-
- print "in-traversal"
- inorder(Root)
-
- print "post-traversal"
- postorder(Root)
-
-
diff --git a/datastruct/graph.py b/datastruct/graph.py
deleted file mode 100644
index 834670b..0000000
--- a/datastruct/graph.py
+++ /dev/null
@@ -1,35 +0,0 @@
-def searchGraph(graph,start,end):
- results=[]
- generatePath(graph,[start],end,results)
- results.sort(lambda x,y:cmp(len(x),len(y)))
- return results
-
-def generatePath(graph,path,end,results):
- state=path[-1]
- if state == end:
- results.append(path)
- else:
- for arc in graph[state]:
- if arc not in path:
- generatePath(graph,path+[arc],end,results)
-
-
-if __name__ == "__main__":
- Graph={
- 'A':['B','C','D'],
- 'B':['E'],
- 'C':['D','F'],
- 'D':['B','E','G'],
- 'E':[],
- 'F':['D','G'],
- 'G':['E']
- }
- r = searchGraph(Graph,'A','D')
- print "A to D"
- for i in r:
- print i
-
- r=searchGraph(Graph,'A','E')
- print "A to E"
- for i in r:
- print i
diff --git a/datastruct/queue.py b/datastruct/queue.py
deleted file mode 100644
index 64da3e4..0000000
--- a/datastruct/queue.py
+++ /dev/null
@@ -1,43 +0,0 @@
-class Queue:
- def __init__(self,size=20):
- self.queue=[]
- self.size=size
- self.end=-1
-
- def setSize(self,size):
- self.size=size
-
- def In(self,element):
- if self.end < self.size -1:
- self.queue.append(element)
- self.end = self.end + 1
- else:
- raise "QueueFull"
-
- def Out(self):
- if self.end != -1:
- element = self.queue[0]
- self.queue=self.queue[1:]
- self.end = self.end-1
- return element
- else:
- raise "QueueEmpty"
-
- def End(self):
- return self.end
-
- def empty(self):
- self.queue=[]
- self.end=-1
-
-if __name__ == "__main__":
-
- queue=Queue()
- for i in range(10):
- queue.In(i)
- print queue.End()
-
- for i in range(10):
- print queue.Out()
-
-
diff --git a/datastruct/sort.py b/datastruct/sort.py
deleted file mode 100644
index b190701..0000000
--- a/datastruct/sort.py
+++ /dev/null
@@ -1,78 +0,0 @@
-class BTree:
- def __init__(self,value):
- self.left=None
- self.data=value
- self.right=None
-
- def insertLeft(self,value):
- self.left=BTree(value)
- return self.left
-
- def insertRight(self,value):
- self.right=BTree(value)
- return self.right
-
- def show(self):
- print self.data
-
-def inorder(node):
- if node.data:
- if node.left:
- inorder(node.left)
- node.show()
- if node.right:
- inorder(node.right)
-
-
-def rinorder(node):
- if node.data:
- if node.right:
- rinorder(node.right)
- node.show()
- if node.left:
- rinorder(node.left)
-
-def insert(node,value):
- if value > node.data:
- if node.right:
- insert(node.right,value)
- else:
- node.insertRight(value)
- else:
- if node.left:
- insert(node.left,value)
- else:
- node.insertLeft(value)
-
-
-if __name__ == "__main__":
-
- l=[88,11,2,33,22,4,55,33,221,34]
- Root=BTree(l[0])
- node=Root
- for i in range(1,len(l)):
- insert(Root,l[i])
-
- print "1---->10"
- inorder(Root)
- print "10--->1"
- rinorder(Root)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/datastruct/stack.py b/datastruct/stack.py
deleted file mode 100644
index e5b65ce..0000000
--- a/datastruct/stack.py
+++ /dev/null
@@ -1,59 +0,0 @@
-class Stack:
- def __init__(self,size=20):
- self.stack= []
- self.size= size;
- self.top= -1
-
- def setSize(self,size):
- self.size=size;
-
- def push(self,element):
- if self.isFull():
- raise "StackOverflow"
- else:
- self.stack.append(element)
- self.top = self.top + 1
-
- def pop(self):
- if self.isEmpty():
- raise "StackUnderflow"
- else:
- element=self.stack[-1]
- self.top=self.top-1;
- del self.stack[-1]
- return element
-
- def Top(self):
- return self.top
-
- def empty(self):
- self.stack=[]
- self.top=-1
-
- def isEmpty(self):
- if self.top == -1:
- return True
- else:
- return False
-
- def isFull(self):
- if self.top == self.size-1:
- return True
- else:
- return False
-
-if __name__ == "__main__":
-
- stack=Stack()
-
- for i in range(10):
- stack.push(i)
- print stack.Top()
-
- for i in range(10):
- print stack.pop()
-
- stack.empty()
- print stack.Top()
-
-
diff --git a/docs/CURATION_POLICY.md b/docs/CURATION_POLICY.md
new file mode 100644
index 0000000..12f87ac
--- /dev/null
+++ b/docs/CURATION_POLICY.md
@@ -0,0 +1,136 @@
+# FlyPython curation policy
+
+This policy defines what belongs in the FlyPython catalog, what evidence a
+resource needs, and how maintainers review changes. It is the reference for
+resource proposals and catalog pull requests.
+
+## Scope
+
+FlyPython catalogs durable resources for Python learning and Python-based AI
+agent development. The catalog favors material that helps readers build skills
+or use a maintained tool safely. It is not a general link directory, product
+launch feed, or hosting location for unrelated applications.
+
+Current learning paths are:
+
+- `foundations`
+- `web-apis`
+- `automation`
+- `ai-agents`
+
+## Source priority
+
+Review sources in this order:
+
+1. Official documentation
+2. Official standards
+3. Official project repositories or sites
+
+The current `source_type` values are `official-docs`, `official-standard`, and
+`official-project`. A new source type requires a schema and policy change in
+the same pull request.
+
+Secondary tutorials, affiliate pages, copied lists, thin SEO pages, and
+unmaintained mirrors are excluded unless the policy is deliberately expanded.
+
+## Required catalog fields
+
+`_data/resources.yml` is the canonical source. It contains a `catalog` object
+and a `resources` list.
+
+The `catalog` object records:
+
+- `reviewed_on`
+- `status`
+- `paths`, where every path has `id`, `title_en`, `title_zh`, `summary_en`,
+ `summary_zh`, and `order`
+
+Every resource records:
+
+- Identity: `id`, `path`, `title`, `url`
+- Classification: `source_type`, `level`, `language`
+- Editorial rationale: `why_en`, `why_zh`
+- Review state: `reviewed_on`, `status`
+- Access and safety: `requires_key`, `risk`
+- Editorial selection: `featured`
+
+Allowed classifications are:
+
+| Field | Values |
+| --- | --- |
+| `path` | `foundations`, `web-apis`, `automation`, `ai-agents` |
+| `source_type` | `official-docs`, `official-standard`, `official-project` |
+| `level` | `beginner`, `intermediate`, `advanced`, `all-levels` |
+| `language` | `en`, `zh`, `multilingual` |
+| `status` | `active` |
+| `risk` | `low`, `medium` |
+| `requires_key`, `featured` | Boolean |
+
+IDs must be stable, lowercase, and unique. A rename needs an explicit migration
+plan because external links or generated anchors may depend on the old ID.
+Review dates use ISO `YYYY-MM-DD` format.
+
+## Acceptance criteria
+
+A resource is eligible when all of these are true:
+
+- Its URL is canonical and controlled by the official publisher or project.
+- Its purpose fits one learning path.
+- The English and Chinese rationales are factual, specific, and human-reviewed.
+- Access requirements, API keys, paid tiers, and material safety risks are
+ represented accurately.
+- Its license and ownership are clear enough for the claims the catalog makes.
+- The maintainer can verify the resource on the stated review date.
+
+Do not describe a project as production-ready without current maintenance,
+licensing, security, and adoption evidence. Popularity alone is not enough.
+
+## Editorial rules
+
+Write short, original rationales that explain why the resource is useful in its
+assigned path. Do not copy marketing claims or large passages from the source.
+Do not use an LLM or web-search API to generate descriptions or classifications.
+A human reviewer remains responsible for each statement.
+
+English and Chinese views must come from the same catalog record. A translation
+should preserve the meaning and limits of the source, not add new claims.
+
+Self-promotion must be disclosed. Maintainers assess it under the same criteria
+as every other proposal.
+
+## Link verification
+
+Internal validation runs on every pull request. Network link fetching runs only
+on the scheduled or manually dispatched GitHub Actions workflow.
+
+A 404 or 410 from the canonical resource is strong removal evidence. A 403, 429,
+timeout, or transient 5xx is a review-needed result, not proof that the resource
+is gone. Review-needed results fail the automated audit so they cannot remain
+hidden behind a green run. DNS, connection, TLS, invalid-URL, and
+redirect-protocol failures also fail the audit, but none of these results alone
+is enough to remove a resource. Retry with rate limits and record the observation
+date. Link checks must refuse private, loopback, link-local, multicast,
+cloud-platform, and cloud metadata targets, including redirect destinations.
+
+## Review cadence and removal
+
+Run the catalog-link audit weekly. Perform an editorial review when a resource
+changes ownership, becomes unmaintained, introduces a material safety concern,
+or receives a substantiated report.
+
+Remove a resource when it is permanently unavailable, outside the catalog scope,
+materially misleading, malicious, or no longer meets the acceptance criteria.
+The pull request should preserve the reason and evidence in its description.
+Do not silently replace a resource with an unrelated alternative under the same
+stable ID.
+
+## Permissions and attribution
+
+This repository currently does not grant a general license to reuse its content
+or code. Public visibility is not permission to copy, redistribute, or relicense
+repository material. Contributors must submit only material they have the right
+to submit and must preserve required notices.
+
+Third-party content keeps its original terms. A catalog entry may link to and
+factually describe a third-party resource, but it must not copy or relicense that
+resource.
diff --git a/expand/1.py b/expand/1.py
deleted file mode 100644
index 3239a25..0000000
--- a/expand/1.py
+++ /dev/null
@@ -1,54 +0,0 @@
-#a = '19'
-#print "please input a int number in 1-100"
-#x = raw_input()
-#if a != x:
-# print "sorry you are wrong!\ninput again:"
-# x = raw_input()
-#else
-# print "you are right"
-#raw_input()
-
-from random import randint
-
-def printNum():
-
- sInput = raw_input(u"请输入你猜的数字(1-100):")
-
- try:
- nInput = int(sInput)
- except (ValueError,TypeError),diag:
- print str(diag)
-
- if(nInput < 1 or nInput > 100):
- print u"你数字不在范围之内,请重新输入"
-
- return nInput
-
-def main():
-
- nValue = randint(1,100)
- nInput = printNum()
- nTotal = 1
-
- while(nValue!= nInput):
-
- if (nValue > nInput):
- print u"你猜的数小了"
-
- elif (nValue < nInput):
- print u"你猜的数大了"
-
- nTotal += 1
- nInput=printNum()
-
- print u"恭喜你猜对了"
- print u"你直到猜对 共猜了%d次" % nTotal
-
- if nTotal < 10:
- print u"恭喜,你的成绩超过了平均水平"
- else:
- print u"很遗憾,你成绩没达到平均水平"
-
-
-if __name__ == "__main__":
- main()
diff --git a/expand/2.py b/expand/2.py
deleted file mode 100644
index 8f1d16f..0000000
--- a/expand/2.py
+++ /dev/null
@@ -1,27 +0,0 @@
-#s = raw_input()
-#i = 0
-#for i in s:
-# print i
-#raw_input()
-
-#扩展成在字符串中把特定字符替换成规定字符
-import re
-def printReplace(srcStr,word):
- for letter in srcStr:
- if letter == word:
- srcStr = srcStr.replace(letter,'!') #字符串不可更改
- return srcStr
-
-def printReplace_re(srcStr,word):
- srcStr=re.sub(word,'!',srcStr)
- return srcStr
-
-def main():
- srcStr = raw_input(u"请输入一个字符串:")
- destStr=printReplace_re(srcStr,'i')
- print destStr
-
-
-if __name__ == "__main__":
- main()
-
diff --git a/index.md b/index.md
new file mode 100644
index 0000000..01f793f
--- /dev/null
+++ b/index.md
@@ -0,0 +1,44 @@
+---
+layout: default
+title: Python Resource Catalog for the AI Era
+description: A reviewed catalog of primary Python sources for foundations, web APIs, automation, and AI agents.
+lang: en-US
+permalink: /
+image:
+ path: /assets/images/og-image.png
+ width: 1200
+ height: 630
+ alt: FlyPython Python Resource Catalog
+---
+{% assign catalog_resources = site.data.resources.resources | where: "status", "active" %}
+
+# Learn Python from sources worth trusting.
+
+FlyPython is a reviewed map of official documentation, standards, and
+first-party project resources. Use the four paths to find the next reliable
+source, then return to the main learning hub for guided projects and context.
+
+
+
+
+ {{ catalog_resources.size }} active resources
+ ·
+ Catalog reviewed {{ site.data.resources.catalog.reviewed_on | escape }}
+ ·
+ Primary sources first
+
+
+{% include catalog-grid.html resources=catalog_resources %}
+
+## How this catalog relates to FlyPython
+
+This repository keeps the broader community resource map public and reviewable.
+The main [FlyPython learning hub](https://flypython.com/) turns a smaller set of
+those sources into practical learning paths, tested examples, and clear next
+steps. Start with the
+[no-key Python agent guide](https://flypython.com/learn/python-ai-agent-roadmap)
+if you want a complete project before choosing an agent framework.
diff --git a/primer/10/card.py b/primer/10/card.py
deleted file mode 100644
index e6bac33..0000000
--- a/primer/10/card.py
+++ /dev/null
@@ -1,33 +0,0 @@
-def safe_float(obj):
- try:
- retval = float(obj)
- except (ValueError,TypeError),diag:
- retval = str(diag)
- return retval
-
-def main():
- log = open("cardlog.txt","w")
- try:
- ccfile = open("carddata.txt","r")
- except IOError,e:
- log.write("no txns this \n")
- log.close()
- return
- txns = ccfile.readlines()
- ccfile.close()
- total = 0.00
- log.write("account log:\n")
-
- for eachTxn in txns:
- result = safe_float(eachTxn)
- if isinstance(result,float):
- total += result
- log.write("data ....processed\n")
- else:
- log.write("ignored: %s" % result)
- print "$%.2f (new balance)" % (total)
- log.close()
-
-if __name__ == '__main__':
- print "run"
- main()
diff --git a/primer/10/myexc.py b/primer/10/myexc.py
deleted file mode 100644
index 0e3245f..0000000
--- a/primer/10/myexc.py
+++ /dev/null
@@ -1,104 +0,0 @@
-
-import os,socket,errno,types,tempfile
-
-class NetworkError(IOError):
- pass
-
-class FileError(IOError):
- pass
-
-def updArgs(args,newarg=None):
- if isinstance(args,IOError):
- myargs =[]
- myargs.extend([arg for arg in args])
- else:
- myargs = list(args)
-
- if newarg:
- myargs.append(newarg)
-
- return tuple(myargs)
-
-def fileAargs(file,mode,agrs):
- if args[0] == errno.EACCES and \
- 'access' in dir(os):
- perms = ''
- perms = {'r':os.R_OK,'w':os.W_OK,'x':os.X_OK}
- pkeys = permd.keys()
- pkeys.sort()
- pkeys.reverse()
-
- for eachPerm in 'rwx':
- if os.access(file,permd[eachPerm]):
- perms += eachPerm
- else:
- perms += '-'
-
- if isinstance(args,IOError):
- myargs = []
- myargs.extend([arg for arg in args])
- else:
- myargs = list(args)
-
- myargs[1] = "'%s' %s (perms:'%s')" % (mode,myargs[1],perms)
-
- myargs.append(args.filename)
-
- else:
- myargs = args
- return tuple(myargs)
-
-def myconnnect(sock,host,port):
- try:
- sock.connect((hosy,port))
- except socket.error,args:
- myargs = updArgs(args)
- if len(myargs) == 1:
- myargs = (errno.ENXIO,myargs[0])
-
- raise NetworkError,\
- updArgs(myargs,host +":" + str(port))
-def myopen(file,mode ="r"):
- try:
- fo = open(file,mode)
- except IOError,args:
- raise FileError,fileArgs(file,mode,args)
- return fo
-
-def testfile():
- file = mkdtemp()
- f = open(file,"w")
- f.close()
-
- for eachTest in ((0,"r"),(0100,"r"), \
- (0400,"w"),(0500,"w")):
- try:
- os.chmod(file,eachTest[0])
- f=myopen(file,eachTest[1])
-
- except FileError,args:
- print "%s: %s" % \
- (args.__class__.__name__,args)
- else:
- print file,"open ok..perm ignored"
- f.close()
- os.chmod(file,0777)
- os.unlink(file)
-def testnet():
- s = socket.socket(socket.AF_INET.socket.SOCK_STREAM)
- for eachHost in ("deli","www"):
- try:
- myconnect(s,"deli",8080)
- except NetworkError,args:
- print "%s:%s" %\
- (args.__class__.__name__,args)
-if __name__ == "__main__":
- testfile()
- testnet()
-
-
-
-
-
-
-
diff --git a/primer/11/Gui.py b/primer/11/Gui.py
deleted file mode 100644
index 6bff1f6..0000000
--- a/primer/11/Gui.py
+++ /dev/null
@@ -1,17 +0,0 @@
-
-from functools import partial
-import Tkinter
-
-root=Tkinter.Tk()
-MyButton = partial(Tkinter.Button,root,
- fg="white",bg="blue")
-b1= MyButton(text="button 1")
-b2=MyButton(text="button 2")
-#qb=MyButton(text="QUIT",bg="red",
- # command=root.quit)
-
-b1.pack()
-b2.pack()
-#qb.pack(file=Tkinter.X,expand= True)
-root.title("PFAs!")
-root.mainloop()
diff --git a/primer/11/deco.py b/primer/11/deco.py
deleted file mode 100644
index 21d7389..0000000
--- a/primer/11/deco.py
+++ /dev/null
@@ -1,22 +0,0 @@
-
-from time import ctime,sleep
-
-def tsfunc(func):
- def wrappedFunc():
- print "[%s] %s() called" % \
- (ctime(),func.__name__)
- return func()
- return wrappendFunc
-
-@tsfunc
-def foo():
- pass
-
-foo()
-sleep(4)
-
-for i in range(2):
- sleep(1)
- foo()
-
-
diff --git a/primer/11/easyMath.py b/primer/11/easyMath.py
deleted file mode 100644
index e14d53b..0000000
--- a/primer/11/easyMath.py
+++ /dev/null
@@ -1,40 +0,0 @@
-
-from operator import add,sub
-from random import randint,choice
-
-ops ={"+":add,"-":sub}
-MAXTRIES = 2
-
-def doprob():
- op = choice('+-')
- nums = [randint(1,10) for i in range(2)]
- nums.sort(reverse = True)
- ans = ops[op](*nums)
- pr="%d %s %d =" %(nums[0],op,nums[1])
- oops =0
- while True:
- try:
- if int(raw_input(pr)) == ans:
- print "corrent"
- break
- if oops == MAXTRIES:
- print "answer \n %s%d" %(pr,ans)
- else:
- print "incorrent...try again"
- oops += 1
- except (KeyboardInterrupt, EOFError,ValueError):
- print "invalid input ... try again"
-
-def main():
- while True:
- doprob()
- try:
- opt= raw_input("Again?[y]".lower())
- if opt and opt[0] == "n":
- break
- except (KeyboardInterrupt,EOFError):
- break
-if __name__ == "__main__":
- main()
-
-
diff --git a/primer/11/grabWeb.py b/primer/11/grabWeb.py
deleted file mode 100644
index 5b3de06..0000000
--- a/primer/11/grabWeb.py
+++ /dev/null
@@ -1,26 +0,0 @@
-
-from urllib import urlretrieve
-
-def firstNoBlank(lines):
- for eachLine in lines:
- if not eachLine.strip():
- continue
- else:
- return eachLine
-
-def firstLast(webpage):
- f = open(webpage)
- lines= f.readlines()
- f.close()
- print firstNoBlank(lines),lines.reverse()
- print firstNoBlank(lines),
-
-def download(url = "http://www.cqupt.edu.cn",process=firstLast):
- try:
- retval = urlretrieve(url)[0]
- except IOError:
- retval = None
- if retval:
- process(retval)
-if __name__ == "__main__":
- download()
diff --git a/primer/11/numconv.py b/primer/11/numconv.py
deleted file mode 100644
index 63eb282..0000000
--- a/primer/11/numconv.py
+++ /dev/null
@@ -1,8 +0,0 @@
-
-def convert(func,seq):
- return[func (eachNum ) for eachNum in seq]
-
-myseq=(123,45.67,-6.2e8,99999999L)
-print convert(int,myseq)
-print convert(long,myseq)
-print convert(float,myseq)
diff --git a/primer/11/odd.py b/primer/11/odd.py
deleted file mode 100644
index 2a37d6a..0000000
--- a/primer/11/odd.py
+++ /dev/null
@@ -1,10 +0,0 @@
-
-from random import randint
-
-#def odd(n):
-# return n%2
-
-allNums =[]
-for eachNum in range(9):
- allNums.append(randint(1,99))
-print[n for n in allNums if n%2]
diff --git a/primer/11/testit.py b/primer/11/testit.py
deleted file mode 100644
index ad5037e..0000000
--- a/primer/11/testit.py
+++ /dev/null
@@ -1,26 +0,0 @@
-
-def testit(func,*nkwargs,**kwargs):
- try:
- retval = func(*nkwargs,**kwargs)
- result = (True,retval)
- except Exception,diag:
- result = (False,str(diag))
- return result
-
-def test():
- funcs =(int,long,float)
- vals = (1234,12.34,"1234","12.34")
-
- for eachFunc in funcs:
- print "_"*20
- for eachVal in vals:
- retval = testit(eachFunc,eachVal)
- if retval[0]:
- print "%s(%s)=" % \
- (eachFunc.__name__,"eachVal"),retval[1]
- else:
- print "%s(%s)=FAILED:" %\
- (eachFunc.__name__,"eachVal"),retval[1]
-
-if __name__ == "__main__":
- test()
diff --git a/primer/12/hot.py b/primer/12/hot.py
deleted file mode 100644
index e695791..0000000
--- a/primer/12/hot.py
+++ /dev/null
@@ -1,10 +0,0 @@
-class HotelRoomCalc(object):
- 'hotel room rate calculator'
- def __init__(self,rt,sales=0.085,rm=0.1):
- self.salesTax = sales
- self.roomTax = rm
- self.roomRate = rt
-
- def calcTotal(self,days = 1):
- daily = round((self.roomRate *(1+self.roomTax + self.salesTax)),2)
- return float(days)*daily
diff --git a/primer/13/NumStr.py b/primer/13/NumStr.py
deleted file mode 100644
index 986062b..0000000
--- a/primer/13/NumStr.py
+++ /dev/null
@@ -1,27 +0,0 @@
-class NumStr(object):
- def __init__(self,num=0,string=''):
- self.__num = num
- self.__string = string
-
- def __str__(self):
- return "[%d :: %r]" % (self.__num,self.__string)
- __repr__ = __str__
-
- def __add__(self,other):
- if isinstance(other,NumStr):
- return self.__class__(self.__num + \
- other.__num,self.__string+other.__string)
- else:
- return TypeError,"type error"
- def __mul__(self,num):
- if isinstance(num,int):
- return self.__class__(self.__num *num,self.__string *num)
- else:
- raise TypeError,"__num__ error"
- def __nonzero__(self):
- return self.__num or len(self.__string)
- def __norm_cval(self,cmpres):
- return cmp(cmpres,0)
- def __cmp__(self,other):
- return self.__norm_cval(cmp(self.__num,other.__num)) + \
- self.__norm_cval(cmp(self.__string,other.__string))
diff --git a/primer/13/Rand.py b/primer/13/Rand.py
deleted file mode 100644
index 1b56d0d..0000000
--- a/primer/13/Rand.py
+++ /dev/null
@@ -1,10 +0,0 @@
-
-from random import choice
-
-class Rand(object):
- def _init_(self,seq):
- self.data = seq
- def _iter_(self):
- return self
- def next(self):
- return choice(self.data)
diff --git a/primer/13/file.py b/primer/13/file.py
deleted file mode 100644
index 8830909..0000000
--- a/primer/13/file.py
+++ /dev/null
@@ -1,34 +0,0 @@
-import os
-import pickle
-
-class File(object):
- saved=[]
- def __init__(self,name=None):
- self.name = name
- def __get__(self,obj,typ=None):
- if self.name not in File.saved:
- raise AttributeError,"%r used before assignment " % self.name
- try:
- f = open(self.name,"r")
- val = pickle.load(f)
- f.close()
- return val
- except (pickle.UnpicklingError,IOError,EOFError,AttributeError,\
- ImportError,IndexError),e:
- raise AttributeError,"could not read %r:%s" % (self.name,e)
- def __set__(self,obj,val):
- f = open(self.name,"w")
- try:
- pickle.dump(val,f)
- File.saved.append(self.name)
- except (TypeError,pickle.PicklingError),e:
- raise AttributeError,"could not pickle %r " % self.name
- finally:
- f.close()
-
- def __delete__(self,obj):
- try:
- os.unlink(self.name)
- File.saved.remove(self.name)
- except (OSError,ValueError),e:
- pass
diff --git a/primer/13/meta.py b/primer/13/meta.py
deleted file mode 100644
index 243a253..0000000
--- a/primer/13/meta.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from warnings import warn
-
-class ReqStr(type):
- def __init__(cls,name,bases,attrd):
- super(ReqStr,cls).__init__(name,bases,attrd)
- if "__str__" not in attrd:
- raise TypeError("class overring __str__")
- if "__repr__" not in attrd:
- warn("class suggests __repr__",stacklevel = 3)
-print "define ReqStr (meta)class \n"
-
-class Foo(object):
- __metaclass__ = ReqStr
-
- def __str__(self):
- return "instance of class",self.__class__.__name__
-print "defined Foo class\n"
-
-class Bar(object):
- __metaclass__ = ReqStr
-
- def __str__(self):
- return self.__class__.__name__
-print "defined Bar class\n "
-
-class FooBar(object):
- __metaclass__ = ReqStr
-print "defined FooBar class \n"
diff --git a/primer/13/roundFloat.py b/primer/13/roundFloat.py
deleted file mode 100644
index 06cf9bf..0000000
--- a/primer/13/roundFloat.py
+++ /dev/null
@@ -1,9 +0,0 @@
-class RoundFloat(object):
- def __init__(self,val):
- assert isinstance(val,float),"Value must be a float"
- self.value = round(val,2)
-
- def __str__(self):
- return "%.2f" %self.value
- __repr__ = __str__
-
diff --git a/primer/13/time.py b/primer/13/time.py
deleted file mode 100644
index de514b5..0000000
--- a/primer/13/time.py
+++ /dev/null
@@ -1,28 +0,0 @@
-
-from time import time,ctime
-
-class Time(object):
- def __init__(self,obj):
- self.__data = obj;
- self.__ctime = self.__mtime =self.__atime=time()
- def get(self):
- self.__atime = time()
- return self.__data
- def gettimeval(self,t_type):
- if not isinstance(t_type,str) or t_type[0] not in "cma":
- raise TypeError," arg c m a"
- return getattr(self,"_%s__%stime" % (self.__class__.__name__,t_type[0]))
- def gettimestr(self,t_type):
- return ctime(self.gettimeval(t_type))
- def set(self,obj):
- self.__data = obj;
- self.__mtime = self.__atime= time()
- def __repr__(self):
- self.__atime = time()
- return 'self.__data'
- def __str__(self):
- self.__atime = time()
- return str(self.__data)
- def __getatt__(self,attr):
- self.__atime = time()
- return getattr(self.__data,attr)
diff --git a/primer/13/time60.py b/primer/13/time60.py
deleted file mode 100644
index a61ca1e..0000000
--- a/primer/13/time60.py
+++ /dev/null
@@ -1,14 +0,0 @@
-class Time60(object):
- def __init__(self,hr,min):
- self.hr = hr
- self.min = min
- def __str__(self):
- return "%d:%d" %(self.hr,self.min)
- __repr__ = __str__
-
- def __add__(self,other):
- return self.__class__(self.hr + other.hr,self.min+other.min)
- def __iadd__(self,other):
- self.hr += other.hr
- self.min += other.min
- return self
diff --git a/primer/16/sockCli.py b/primer/16/sockCli.py
deleted file mode 100644
index f97acee..0000000
--- a/primer/16/sockCli.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from socket import *
-
-HOST ="localhost"
-PORT = 8888
-BUFSIZ = 1024
-ADDR = (HOST,PORT)
-
-while True:
- tcpCliSock = socket(AF_INET,SOCK_STREAM)
- tcpCliSock.connect(ADDR)
- data=raw_input(">")
-
- if not data:
- break
- tcpCliSock.send("%s\r\n" % data)
- data = tcpCliSock.recv(BUFSIZ)
- if not data:
- break
- print data.strip()
- tcpCliSock.close()
diff --git a/primer/16/sockServ.py b/primer/16/sockServ.py
deleted file mode 100644
index 9e4e38f..0000000
--- a/primer/16/sockServ.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from SocketServer import (TCPServer as TCP,
- StreamRequestHandler as SRH)
-from time import ctime
-
-HOST=''
-PORT=8888
-ADDR=(HOST,PORT)
-
-class MyRequestHandler(SRH):
- def handle(self):
- print "......connected from :",self.client_address
- self.wfile.write('[%s] %s' %
- (ctime(),self.rfile.readline()))
-
-tcpSer = TCP(ADDR,MyRequestHandler)
-print "waiting for connection..."
-tcpSer.serve_forever()
diff --git a/primer/16/tcpclient.py b/primer/16/tcpclient.py
deleted file mode 100644
index b346fc6..0000000
--- a/primer/16/tcpclient.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from socket import *
-
-HOST = "localhost"
-PORT = 21567
-BUFSIZ = 1024
-ADDR=(HOST,PORT)
-
-tcpCliSock = socket(AF_INET,SOCK_STREAM)
-tcpCliSock.connect(ADDR)
-
-while True:
- data = raw_input(">")
- if not data:
- break
- tcpCliSock.send(data)
- data = tcpCliSock.recv(BUFSIZ)
- if not data:
- break
- print data
-
-tcpCliSock.close()
diff --git a/primer/16/tcpserver.py b/primer/16/tcpserver.py
deleted file mode 100644
index 688b0aa..0000000
--- a/primer/16/tcpserver.py
+++ /dev/null
@@ -1,26 +0,0 @@
-
-from socket import *
-from time import ctime
-
-HOST =''
-PORT=21567
-BUFSIZ = 1024
-ADDR =(HOST,PORT)
-
-tcpSerSock = socket(AF_INET,SOCK_STREAM)
-tcpSerSock.bind(ADDR)
-tcpSerSock.listen(5)
-
-while True:
- print "waiting from connection..."
- tcpCliSock,addr = tcpSerSock.accept()
- print "...connected from :",addr
-
- while True:
- data = tcpCliSock.recv(BUFSIZ)
- if not data:
- break
- tcpCliSock.send('[%s] %s' %(ctime(),data))
-
- tcpCliSock.close()
-tcpSerSock.close()
diff --git a/primer/16/udpCli.py b/primer/16/udpCli.py
deleted file mode 100644
index 9a502f3..0000000
--- a/primer/16/udpCli.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from socket import *
-
-HOST="localhost"
-PORT=21567
-BUFSIZ = 1024
-ADDR= (HOST,PORT)
-
-udpCliSock= socket(AF_INET,SOCK_DGRAM)
-
-while True:
- data = raw_input(">")
- if not data:
- break
- udpCliSock.sendto(data,ADDR)
- data,ADDR= udpCliSock.recvfrom(BUFSIZ)
- if not data:
- break
- print data
-udpCliSock.close()
diff --git a/primer/16/udpSer.py b/primer/16/udpSer.py
deleted file mode 100644
index bf7eabb..0000000
--- a/primer/16/udpSer.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from socket import *
-from time import ctime
-
-HOST =''
-PORT=21567
-BUFSIZ=1024
-ADDR =(HOST,PORT)
-
-udpSerSock = socket(AF_INET,SOCK_DGRAM)
-udpSerSock.bind(ADDR)
-
-while True:
- print "waiting for message..."
- data,addr = udpSerSock.recvfrom(BUFSIZ)
- udpSerSock.sendto('[%s] %s' %(ctime(),data),addr)
- print "...received from and returned to :",addr
-udpSerSock.close()
diff --git a/primer/18/sleep.py b/primer/18/sleep.py
deleted file mode 100644
index 65c80cd..0000000
--- a/primer/18/sleep.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from time import sleep,ctime
-import thread
-
-loops =[4,2,11]
-def loop(nloop,nsec,lock):
- print " start loop:",nloop,"at",ctime()
- sleep(nsec)
- print "loop :",nloop, "done at:",ctime()
-
-def main():
- print "start at:", ctime()
- locks = []
- nloops = range(len(loops))
-
- for i in nloops:
- lock = thread.allocate_lock()
- lock.acquire()
- locks.append(lock)
- for i in nloops:
- thread.start_new_thread(loop,(i,loops[i],locks[i]))
- for i in nloops:
- while locks[i].locked():pass
-
- print " all done at:" ,ctime()
-if __name__ == "__main__":
- main()
diff --git a/primer/18/threading.py b/primer/18/threading.py
deleted file mode 100644
index 367fcf3..0000000
--- a/primer/18/threading.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import threading
-from time import sleep,ctime
-
-loops = [4,2]
-
-class ThreadFunc(object):
- def __init__(self,func,args,name=""):
- self.name = name
- self.name = func
- self.args = args
- def __call__(self):
- apply(self.func,slef.args)
-
- def loop(nloop,nsec):
- print "start loop:",nloop,"at:",ctime()
- sleep(nsec)
- print "loop",nloop,"done at:",ctime()
-
- def main():
- print "starting at:",ctime()
- threads = []
- nloops = range(len(loops))
- #创建线程
- for i in nloops:
- t = threading.Thread(target = loop,args=(i,loops[i]))
- threads.append(t);
- #启动线程
- for i in nloops:
- threads[i].start()
- #等待线程
- for i in nloops:
- threads[i].join()
- print "all DONE at:",ctime()
-
-if __name__ == "__main__":
- main()
-
diff --git a/primer/19/label.py b/primer/19/label.py
deleted file mode 100644
index cec761f..0000000
--- a/primer/19/label.py
+++ /dev/null
@@ -1,11 +0,0 @@
-import Tkinter
-
-top = Tkinter.Tk()
-
-label = Tkinter.Label(top,text="input")
-label.pack()
-
-button = Tkinter.Button(top,text="QIUT",command=top.quit,bg="red",fg="white")
-button.pack(fill=Tkinter.X,expand=1)
-
-Tkinter.mainloop()
diff --git a/primer/2/2_10.py b/primer/2/2_10.py
deleted file mode 100644
index d2dec79..0000000
--- a/primer/2/2_10.py
+++ /dev/null
@@ -1,10 +0,0 @@
-a = 19
-print "please input a int number in 1-100"
-x = raw_input()
-while x != a:
- print "sorry error: \n input again:"
- x = raw_input()
-if x == a:
- print "you are right"
- break
-raw_input()
diff --git a/primer/2/2_11.py b/primer/2/2_11.py
deleted file mode 100644
index 5c3877d..0000000
--- a/primer/2/2_11.py
+++ /dev/null
@@ -1,25 +0,0 @@
-print "Menu: input the choice\n"
-print "s: sum"
-print "a: avg"
-print "x: exit"
-aa =[1,2,3,4]
-while True:
- ch = raw_input()
- if ch == 's':
- i = 0
- Sum = 0
- print "you choice sum:\n"
- for i in aa:
- Sum += i
- print Sum
- if ch == 'a':
- print "you choice avg:\n"
- i = 0
- Sum1 = 0
- for i in aa:
- Sum1 += i
- avg = float(Sum1)/(len(aa))
- print avg
- if ch == 'x':
- break
-
diff --git a/primer/2/2_2.py b/primer/2/2_2.py
deleted file mode 100644
index 5d71d06..0000000
--- a/primer/2/2_2.py
+++ /dev/null
@@ -1,9 +0,0 @@
-a = '19'
-print "please input a int number in 1-100"
-x = raw_input()
-if a != x:
- print "sorry you are wrong!\ninput again:"
- x = raw_input()
-else
- print "you are right"
-raw_input()
diff --git a/primer/2/2_7.py b/primer/2/2_7.py
deleted file mode 100644
index 8405c4b..0000000
--- a/primer/2/2_7.py
+++ /dev/null
@@ -1,5 +0,0 @@
-s = raw_input()
-i = 0
-for i in s:
- print i
-raw_input()
diff --git a/primer/2/2_8.py b/primer/2/2_8.py
deleted file mode 100644
index 7ea33df..0000000
--- a/primer/2/2_8.py
+++ /dev/null
@@ -1,7 +0,0 @@
-s=[1,2,3,4,5,6]
-i = 0
-t = 0
-for i in s:
- t += i
-print float(t)/len(s)
-raw_input()
diff --git a/primer/20/Cralwer.py b/primer/20/Cralwer.py
deleted file mode 100644
index 3afa5d2..0000000
--- a/primer/20/Cralwer.py
+++ /dev/null
@@ -1,114 +0,0 @@
-#!/usr/bin/env python
-
-from sys import argv
-from os import makedirs,unlink,sep
-from os.path import dirname,exists,isdir,splitext
-from string import replace,find,lower
-from htmllib import HTMLParser
-from urllib import urlretrieve
-from urlparse import urlparse,urljoin
-from formatter import DumbWriter,AbstractFormatter
-from cStringIO import StringIO
-
-class Retriever(object):#下载网页类
-
- def __init__(self,url):
- self.url = url
- self.file = self.filename(url)
-
- def filename(self,url,deffile ="index.htm"):
- parsedurl = urlparse(url,"http:",0) #解析路径
- path = parsedurl[1] + parsedurl[2]
- ext = splitext(path)
- if ext[1] == "": #如果没有文件,使用默认
- if path[-1] == "/":
- path += deffile
- else:
- path += "/" + deffile
- ldir = dirname(path) #本地目录
- if sep != "/":
- ldir = replace(ldir,"/",sep)
- if not isdir(ldir): #如果没有目录,创建一个
- if exists(ldir):unlink(ldir)
- makedirs(ldir)
- return path
-
- def download(self):# 下载网页
- try:
- retval = urlretrieve(self.url,self.file)
- except IOError:
- retval = ('***Error: invalid URL: "%s"' % self.url,)
- return retval
-
- def parseAndGetLinks(self): #解析HTML,保存链接
- self.parser = HTMLParser(AbstractFormatter(DumbWriter(StringIO())))
- self.parser.feed(open(self.file).read())
- self.parser.close()
- return self.parser.anchorlist
-
-
-class Crawler(object): #管理类,管理整个爬行过程
-
- count = 0 #下载网页计数器
-
- def __init__ (self,url):
- self.q = [url] #链接队列
- self.seen = [] #已下载
- self.dom = urlparse(url)[1] #判断链接是否为主链接的子域名
-
- def getPage(self,url): #下载网页
- r = Retriever(url)
- retval = r.download()
- if retval[0] == "*": #错误,不解析
- print retval,"--- skipping parse"
- return
- Crawler.count += 1
- print '\n(',Crawler.count,')'
- print "URL:",url
- print "FILE:",retval[0]
- self.seen.append(url)
-
- links = r.parseAndGetLinks() #得到链接
- for eachLink in links:
- if eachLink[:4] != "http" and find(eachLink,"://") == -1:
- eachLink = urljoin(url,eachLink)
- print "* ",eachLink
-
- if find(lower(eachLink),"mailto:") != -1: #过滤邮箱链接
- print "--- discarded,mailto link"
- continue
-
- if eachLink not in self.seen:
- if find(eachLink,self.dom) == -1:
- print "---discarded,not in domain"
- else:
- if eachLink not in self.q:
- self.q.append(eachLink)
- print "---new,add to Q"
- else:
- print "---discarded,already in Q"
- else:
- print "---discarded, arlready processed"
-
- def go(self): #在队列里处理链接,启动
- while self.q:
- url=self.q.pop()
- self.getPage(url)
-
-def main():
- if len(argv) > 1:
- url = argv[1]
-
- else:
- try:
- url = raw_input("Enter starting URL:")
- except (KeyboardInterrupt,EOFError):
- url = ""
-
- if not url: return
- robot = Crawler(url)
- robot.go()
-
-if __name__ == "__main__":
- main()
-
diff --git a/primer/3/makeFile.py b/primer/3/makeFile.py
deleted file mode 100644
index bee369a..0000000
--- a/primer/3/makeFile.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import os
-
-filename=raw_input("filename:")
-if os.path.exists(filename):
- print "file exist"
- exit()
-
-
-fd=open(filename,'r')
-fd.writelines(all)
-fd.close()
-
-print "done"
-
diff --git a/primer/3/makeTextFile.py b/primer/3/makeTextFile.py
deleted file mode 100644
index 6969f8f..0000000
--- a/primer/3/makeTextFile.py
+++ /dev/null
@@ -1,32 +0,0 @@
-
-'makeTextFile.py -- create text file'
-
-import os
-ls = os.linesep
-
-#get filename
-while True:
- fname = raw_input()
- if os.path.exists(fname):
- print "ERROR: '%s' already exists " %fname
- else:
- break
-
-#get file content lines
-all = []
-print "\n Enter lines:('.' by itself to quit)\n"
-
-#loop until user terminates input
-while True:
- entry = raw_input('>')
- if entry == '.':
- break
- else:
- all.append(entry)
-
-#write lines to file with proper line-ending
-fobj = open(fname,'w')
-fobj.writelines(['%s%s' %(x,ls) for x in all])
-fobj.close()
-print 'DONE!'
-
diff --git a/primer/3/readTextFile.py b/primer/3/readTextFile.py
deleted file mode 100644
index 980d3e0..0000000
--- a/primer/3/readTextFile.py
+++ /dev/null
@@ -1,17 +0,0 @@
-
-'readTextFile.py -- read and display text file'
-
-#get filename
-fname = raw_input("enter the filename:")
-print
-
-#attempt to open file for reading
-try:
- fobj = open(fname,'r')
-except IOError,e:
- print "*** file open error",e
-else:
- #display contents to the screen
- for echoLine in fobj:
- print echoLine;
-fobj.close()
diff --git a/primer/4/display.py b/primer/4/display.py
deleted file mode 100644
index e50f368..0000000
--- a/primer/4/display.py
+++ /dev/null
@@ -1,11 +0,0 @@
-def displayType(num):
- print num, 'is',
- if isinstance(num,(int,long,float,complex)):
- print "a number of type:",type(num).__name__
- else:
- print "not a number at all"
-
-displayType(2)
-displayType(-1.9)
-displayType(-1+1.9j)
-displayType('xcxx')
diff --git a/primer/6/idcheck.py b/primer/6/idcheck.py
deleted file mode 100644
index b6d6220..0000000
--- a/primer/6/idcheck.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import string
-
-alphas = string.letters +'_'
-nums = string.digits
-
-print "Welcom to the indetifter Checker V1.0"
-print "Test must be at least 2 Chars long"
-myinput = raw_input("Iddentifter to test:\n")
-
-if len(myinput) > 1:
- if myinput[0] not in alphas:
- print "invald: first symbol must be alphbetic"
- else:
- for otherChar in myinput[1:]:
- if alphas not in alphas + nums:
- print "invalid:remaining symbols must be alphanumeric"
- break
- else:
- print "ok as an identifier"
-raw_input()
diff --git a/primer/6/queue.py b/primer/6/queue.py
deleted file mode 100644
index 7eb364d..0000000
--- a/primer/6/queue.py
+++ /dev/null
@@ -1,38 +0,0 @@
-
-queue = []
-def enQ():
- queue.append(raw_input("enter new string:").strip())
-def deQ():
- if len(queue) == 0:
- print "empty queue\n"
- else:
- print 'Removed![',`queue.pop()`,']'
-def viewQ():
- print queue
-
-CMDS = {'e': enQ,'d':deQ,'v':viewQ}
-def showmenu():
- pr="""
-(E)nqueue
-(D)enqueue
-(V)iew
-(Q)uit
-
- enter choice: """
- while True:
- while True:
- try:
- choice = raw_input(pr).strip()[0].lower()
- except (EOFError,KeyboardInterpt,IndexError):
- choice = "q"
- print "\nYou picked:[%s]" % choice
- if choice not in "devq":
- print "error,try again"
- else:
- break
- if choice == "q":
- break
- CMDS[choice]()
-if __name__ == '__main__':
- showmenu()
-
diff --git a/primer/6/stack.py b/primer/6/stack.py
deleted file mode 100644
index 60dc053..0000000
--- a/primer/6/stack.py
+++ /dev/null
@@ -1,43 +0,0 @@
-stack = []
-
-def pushit():
- stack.append(raw_input("enter a string:").strip())
-
-def popit():
- if len(stack) == 0:
- print "stack is empty!\n"
- else:
- print 'Removed![',`stack.pop()`,']'
-
-def viewstack():
- print stack
-
-CMDS ={'u': pushit,'o':popit,'v':viewstack}
-
-def showmenu():
- pr = """
- p(U)sh
- p(O)p
- (V)iew
- (Q)uit
-
- enter the choice: """
- while True:
- while True:
- try:
- choice = raw_input(pr).strip()[0].lower()
- except (EOFError,KeyboardInterrupt,IndexError):
- choice ="q"
-
- print "\nYou picked:[%s]" % choice
- if choice not in "uovq":
- print "Invalid option,try again"
- else:
- break
-
- if choice == "q":
- break
- CMDS[choice]()
-if __name__ == "__main__":
- showmenu()
-
diff --git a/primer/6/unicode.txt b/primer/6/unicode.txt
deleted file mode 100644
index f3e8683..0000000
--- a/primer/6/unicode.txt
+++ /dev/null
@@ -1 +0,0 @@
-hhhh
diff --git a/primer/6/unicodeFile.py b/primer/6/unicodeFile.py
deleted file mode 100644
index 0752725..0000000
--- a/primer/6/unicodeFile.py
+++ /dev/null
@@ -1,20 +0,0 @@
-'''
-An example for unicode string
-'''
-
-CODEC = "utf-8"
-FILE = "unicode.txt"
-
-hello = u"hhhh\n"
-byte = hello.encode(CODEC)
-f = open(FILE,"w")
-f.write(byte)
-f.close()
-
-f = open(FILE,"r")
-byte = f.read()
-f.close()
-
-hello = byte.decode(CODEC)
-print hello
-raw_input()
diff --git a/primer/7/userpw.py b/primer/7/userpw.py
deleted file mode 100644
index ebe358e..0000000
--- a/primer/7/userpw.py
+++ /dev/null
@@ -1,53 +0,0 @@
-db = {}
-
-def newuser():
- prompt = "login desired:"
- while True:
- name = raw_input(prompt)
- if db.has_key(name):
- prompt = "name taken,try again: "
- continue
- else:
- break
- pwd = raw_input("passwd:")
- db[name]= pwd
- print "regeisted oK!\n"
-
-def olduser():
- name = raw_input("login:")
- pwd = raw_input("passwd:")
- passwd = db.get(name)
- if passwd == pwd:
- print "welcome back," ,name
- else:
- print "login incorrect"
-
-def showmenu():
- prompt = """
- (n) new user login
- (l) exiting user login
- (q) quit
- enter choice : """
-
- done = False
- while not done:
- chosen = False
- while not chosen:
- try:
- choice = raw_input(prompt).strip()[0].lower()
- except(EOFError,KeyboardInterrupt):
- choice = "q"
- print "\n you picked [%s]" % choice
- if choice not in "nlq":
- print "invalid option, try again"
- else:
- chosen = True
-
- if choice == "q": done = True
- if choice == "n": newuser()
- if choice == "l": olduser()
-
-if __name__ == "__main__":
- showmenu()
-
-
diff --git a/primer/9/ospathex.py b/primer/9/ospathex.py
deleted file mode 100644
index a333211..0000000
--- a/primer/9/ospathex.py
+++ /dev/null
@@ -1,38 +0,0 @@
-
-import os
-for tmpdir in("/tmp"):
- if os.path.isdir(tmpdir):
- break
- else:
- print "no temp dir available"
- tmpdir = ""
-if tmpdir:
- os.chdir(tmpdir)
- cwd = os.getcwd()
- print " current tmp dir"
- print cwd
-
- print "create example dir"
- os.mkdir("example")
- os.chdir("example")
- cwd = os.getcwd()
- print "new work dir"
- print cwd
- print "list the dir"
- print os.listdir(cwd)
-
- print "create test file"
- fobj = open("test","w")
- fobj.write("xxg\n")
- fobj.write("111111")
- fobj.close()
-
- print "update the list dir"
- print os.listdir(cwd)
-
- print "rename the file"
- os.rename("test","xxg.txt")
- print os.listdir(cwd)
-
-
-
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..68ffc92
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[tool.pytest.ini_options]
+addopts = "-q"
+testpaths = ["tests"]
diff --git a/requirements-dev.lock.txt b/requirements-dev.lock.txt
new file mode 100644
index 0000000..353ef54
--- /dev/null
+++ b/requirements-dev.lock.txt
@@ -0,0 +1,6 @@
+-r requirements.lock.txt
+iniconfig==2.3.0
+packaging==26.3
+pluggy==1.6.0
+Pygments==2.21.0
+pytest==9.1.1
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..1441c92
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,2 @@
+-r requirements.txt
+pytest==9.1.1
diff --git a/requirements.lock.txt b/requirements.lock.txt
new file mode 100644
index 0000000..bbdff30
--- /dev/null
+++ b/requirements.lock.txt
@@ -0,0 +1,6 @@
+PyYAML==6.0.3
+certifi==2026.7.22
+charset-normalizer==3.5.1
+idna==3.19
+requests==2.32.5
+urllib3==2.6.3
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..4d2c6a5
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+PyYAML==6.0.3
+requests==2.32.5
+urllib3==2.6.3
diff --git a/robots.txt b/robots.txt
new file mode 100644
index 0000000..3b1950c
--- /dev/null
+++ b/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://python.flypython.com/sitemap.xml
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..9d9d70d
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,51 @@
+from __future__ import annotations
+
+from datetime import date
+from typing import Any
+
+import pytest
+
+
+@pytest.fixture
+def valid_catalog() -> dict[str, Any]:
+ paths = []
+ resources = []
+ for order, path_id in enumerate(
+ ("foundations", "web-apis", "automation", "ai-agents"), start=1
+ ):
+ paths.append(
+ {
+ "id": path_id,
+ "title_en": f"{path_id} title",
+ "title_zh": f"{path_id} 标题",
+ "summary_en": f"{path_id} summary",
+ "summary_zh": f"{path_id} 摘要",
+ "order": order,
+ }
+ )
+ resources.append(
+ {
+ "id": f"resource-{order}",
+ "path": path_id,
+ "title": f"Resource {order}",
+ "url": f"https://example{order}.com/docs/",
+ "source_type": "official-docs",
+ "level": "beginner",
+ "language": "en",
+ "why_en": "Primary documentation maintained by the project.",
+ "why_zh": "由项目维护的官方文档。",
+ "reviewed_on": date(2026, 8, 31),
+ "status": "active",
+ "requires_key": False,
+ "risk": "low",
+ "featured": order == 1,
+ }
+ )
+ return {
+ "catalog": {
+ "reviewed_on": date(2026, 8, 31),
+ "status": "active",
+ "paths": paths,
+ },
+ "resources": resources,
+ }
diff --git a/tests/render_security_test.rb b/tests/render_security_test.rb
new file mode 100644
index 0000000..cefb831
--- /dev/null
+++ b/tests/render_security_test.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require "jekyll"
+require "tmpdir"
+
+source = File.expand_path("..", __dir__)
+
+Dir.mktmpdir("flypython-render-security") do |destination|
+ config = Jekyll.configuration(
+ "source" => source,
+ "destination" => destination,
+ "quiet" => true,
+ "disable_disk_cache" => true
+ )
+ site = Jekyll::Site.new(config)
+ site.reset
+ site.read
+
+ path = site.data.fetch("resources").fetch("catalog").fetch("paths").first
+ path["title_en"] = "
"
+ path["title_zh"] = "
"
+ path["summary_en"] = "